feat(query-params): proper handling in case of invalid credentials

This commit is contained in:
Benedikt Rötsch
2018-02-07 14:18:19 +01:00
committed by Benedikt Rötsch
parent ff305e7d34
commit 46074b508d
6 changed files with 147 additions and 71 deletions

53
app.js
View File

@@ -13,11 +13,14 @@ require('dotenv').config({ path: 'variables.env' })
const helpers = require('./helpers')
const { translate, initializeTranslations } = require('./i18n/i18n')
const breadcrumb = require('./lib/breadcrumb')
const { updateCookie } = require('./lib/cookies')
const settings = require('./lib/settings')
const routes = require('./routes/index')
const { getSpace } = require('./services/contentful')
const { catchErrors } = require('./handlers/errorHandlers')
const SETTINGS_NAME = 'theExampleAppSettings'
const app = express()
// View engine setup
@@ -53,27 +56,11 @@ app.use(catchErrors(async function (request, response, next) {
response.locals.helpers = helpers
// Make query string available in templates to render links properly
const qs = querystring.stringify(request.query)
// Creates a query string which adds the current credentials to links
// To other implementations of this app in the about modal
let settingsQuery = {
editorial_features: response.locals.settings.editorialFeatures ? 'enabled' : 'disabled'
}
if (
response.locals.settings.spaceId !== process.env.CONTENTFUL_SPACE_ID ||
response.locals.settings.deliveryToken !== process.env.CONTENTFUL_DELIVERY_TOKEN ||
response.locals.settings.previewToken !== process.env.CONTENTFUL_PREVIEW_TOKEN
) {
settingsQuery = Object.assign({}, settingsQuery, request.query, {
space_id: response.locals.settings.spaceId,
delivery_token: response.locals.settings.deliveryToken,
preview_token: response.locals.settings.previewToken
})
}
const cleanQuery = helpers.cleanupQueryParameters(request.query)
const qs = querystring.stringify(cleanQuery)
const settingsQs = querystring.stringify(settingsQuery)
response.locals.queryString = qs ? `?${qs}` : ''
response.locals.queryStringSettings = settingsQs ? `?${settingsQs}` : ''
response.locals.queryStringSettings = response.locals.queryString
response.locals.query = request.query
response.locals.currentPath = request.path
@@ -93,12 +80,24 @@ app.use(catchErrors(async function (request, response, next) {
}
]
// Set currently used api
response.locals.currentApi = apis
.find((api) => api.id === (request.query.api || 'cda'))
const space = await getSpace()
response.locals.locales = space.locales
next()
}))
// Test space connection and attach space related data for views if possible
app.use(catchErrors(async function (request, response, next) {
// Catch misconfigured space credentials and display settings page
try {
const space = await getSpace()
// Update credentials in cookie when space connection is successful
updateCookie(response, SETTINGS_NAME, settings)
// Get available locales from space
response.locals.locales = space.locales
const defaultLocale = response.locals.locales
.find((locale) => locale.default)
@@ -110,6 +109,18 @@ app.use(catchErrors(async function (request, response, next) {
if (!response.locals.currentLocale) {
response.locals.currentLocale = defaultLocale
}
// Creates a query string which adds the current credentials to links
// To other implementations of this app in the about modal
helpers.updateSettingsQuery(request, response, response.locals.settings)
} catch (error) {
if ([401, 404].includes(error.response.status)) {
// If we can't connect to the space, force the settings page to be shown.
response.locals.forceSettingsRoute = true
} else {
throw error
}
}
next()
}))

View File

@@ -1,4 +1,6 @@
const marked = require('marked')
const querystring = require('querystring')
const { translate } = require('./i18n/i18n')
// Parse markdown text
@@ -19,7 +21,7 @@ module.exports.formatMetaTitle = (title, localeCode = 'en-US') => {
return `${title.charAt(0).toUpperCase()}${title.slice(1)}${translate('defaultTitle', localeCode)}`
}
module.exports.isCustomCredentials = (settings) => {
function isCustomCredentials (settings) {
const spaceId = process.env.CONTENTFUL_SPACE_ID
const deliveryToken = process.env.CONTENTFUL_DELIVERY_TOKEN
const previewToken = process.env.CONTENTFUL_PREVIEW_TOKEN
@@ -29,6 +31,38 @@ module.exports.isCustomCredentials = (settings) => {
settings.previewToken !== previewToken
}
function cleanupQueryParameters (query) {
const cleanQuery = Object.assign({}, query)
delete cleanQuery.space_id
delete cleanQuery.delivery_token
delete cleanQuery.preview_token
delete cleanQuery.reset
return cleanQuery
}
function updateSettingsQuery (request, response, settings) {
const cleanQuery = cleanupQueryParameters(request.query)
let settingsQuery = Object.assign({}, cleanQuery, {
editorial_features: settings.editorialFeatures ? 'enabled' : 'disabled'
})
if (isCustomCredentials(settings)) {
settingsQuery = Object.assign(settingsQuery, {
space_id: settings.spaceId,
delivery_token: settings.deliveryToken,
preview_token: settings.previewToken
})
}
const settingsQs = querystring.stringify(settingsQuery)
response.locals.queryStringSettings = settingsQs ? `?${settingsQs}` : ''
}
module.exports.isCustomCredentials = isCustomCredentials
module.exports.cleanupQueryParameters = cleanupQueryParameters
module.exports.updateSettingsQuery = updateSettingsQuery
/**
* Evil users might try to add base64 url data to execute js code
* so we should purge any potentially harmful data to mitigate risk

View File

@@ -4,9 +4,6 @@
*/
const { initClients } = require('../services/contentful')
const { updateCookie } = require('./cookies')
const SETTINGS_NAME = 'theExampleAppSettings'
module.exports = async function settingsMiddleware (request, response, next) {
// Set default settings based on environment variables
@@ -28,7 +25,6 @@ module.exports = async function settingsMiddleware (request, response, next) {
deliveryToken: delivery_token,
previewToken: preview_token
}
updateCookie(response, SETTINGS_NAME, settings)
}
// Allow enabling and disabling of editorial features via query parameters
@@ -37,7 +33,6 @@ module.exports = async function settingsMiddleware (request, response, next) {
if (typeof editorial_features !== 'undefined') {
delete request.query.editorial_features
settings.editorialFeatures = editorial_features === 'enabled'
updateCookie(response, SETTINGS_NAME, settings)
}
/* eslint-enable camelcase */

View File

@@ -12,6 +12,15 @@ const { getSettings, postSettings } = require('./settings')
const { getLandingPage } = require('./landingPage')
const { getImprint } = require('./imprint')
// Display settings in case of invalid credentials
router.all('*', async (request, response, next) => {
if (response.locals.forceSettingsRoute) {
await getSettings(request, response, next)
return
}
next()
})
// GET the home landing page
router.get('/', catchErrors(getLandingPage))

View File

@@ -2,14 +2,15 @@
* This module renders the settings page when `settings` route is requested
* it also saves the settings to a cookie
*/
const { uniqWith, isEqual } = require('lodash')
const { createClient } = require('contentful')
const { initClients, getSpace } = require('./../services/contentful')
const { isCustomCredentials, updateSettingsQuery } = require('../helpers')
const { updateCookie } = require('../lib/cookies')
const { translate } = require('../i18n/i18n')
const { uniqWith, isEqual } = require('lodash')
const SETTINGS_NAME = 'theExampleAppSettings'
const { initClients, getSpace } = require('../services/contentful')
const querystring = require('querystring')
const SETTINGS_NAME = 'theExampleAppSettings'
async function renderSettings (response, opts) {
// Get connected space to display the space name on top of the settings
@@ -17,9 +18,8 @@ async function renderSettings (response, opts) {
try {
space = await getSpace()
} catch (error) {
// We throw the error here, it will be handled by the error middleware
// We keep space false to ensure the "Connected to" box is not shown.
throw (error)
// We handle errors within the settings page.
// No need to throw here.
}
response.render('settings', {
@@ -42,9 +42,23 @@ async function renderSettings (response, opts) {
* @returns {undefined}
*/
module.exports.getSettings = async (request, response, next) => {
const currentLocale = response.locals.currentLocale
const { settings } = response.locals
const errorList = await generateErrorList(settings, currentLocale)
// If no errors detected, update app to use new settings
if (!errorList.length) {
applyUpdatedSettings(request, response, settings)
}
const errors = generateErrorDictionary(errorList)
await renderSettings(response, {
settings
settings,
errors,
hasErrors: errorList.length > 0,
success: isCustomCredentials(settings) && errorList.length === 0
})
}
@@ -60,7 +74,6 @@ module.exports.getSettings = async (request, response, next) => {
*/
module.exports.postSettings = async (request, response, next) => {
const currentLocale = response.locals.currentLocale
let errorList = []
let { spaceId, deliveryToken, previewToken, editorialFeatures } = request.body
if (request.query.reset) {
@@ -76,6 +89,27 @@ module.exports.postSettings = async (request, response, next) => {
editorialFeatures: !!editorialFeatures
}
const errorList = await generateErrorList(settings, currentLocale)
// If no errors detected, update app to use new settings
if (!errorList.length) {
applyUpdatedSettings(request, response, settings)
}
const errors = generateErrorDictionary(errorList)
await renderSettings(response, {
settings,
errors,
hasErrors: errorList.length > 0,
success: errorList.length === 0
})
}
async function generateErrorList (settings, currentLocale) {
const { spaceId, deliveryToken, previewToken } = settings
let errorList = []
// Validate required fields.
if (!spaceId) {
errorList.push({
@@ -157,29 +191,16 @@ module.exports.postSettings = async (request, response, next) => {
}
}
}
errorList = uniqWith(errorList, isEqual)
// If no errors, then cache the new settings in the cookie
if (!errorList.length) {
// Store new settings
updateCookie(response, SETTINGS_NAME, settings)
response.locals.settings = settings
const settingsQuery = Object.assign({}, request.query, {
space_id: response.locals.settings.spaceId,
delivery_token: response.locals.settings.deliveryToken,
preview_token: response.locals.settings.previewToken,
editorial_features: response.locals.settings.editorialFeatures ? 'enabled' : 'disabled'
})
const settingsQs = querystring.stringify(settingsQuery)
response.locals.queryStringSettings = settingsQs ? `?${settingsQs}` : ''
// Reinit clients
initClients(settings)
return errorList
}
// Generate error dictionary
// Format: { FIELD_NAME: [array, of, error, messages] }
const errors = errorList.reduce((errors, error) => {
function generateErrorDictionary (errorList) {
return errorList.reduce((errors, error) => {
return {
...errors,
[error.field]: [
@@ -188,10 +209,16 @@ module.exports.postSettings = async (request, response, next) => {
]
}
}, {})
await renderSettings(response, {
settings,
errors,
hasErrors: errorList.length > 0,
success: errorList.length === 0
})
}
function applyUpdatedSettings (request, response, settings) {
// Store new settings
updateCookie(response, SETTINGS_NAME, settings)
response.locals.settings = settings
// Update query settings string
updateSettingsQuery(request, response, settings)
// Reinit clients
initClients(settings)
}

View File

@@ -64,7 +64,7 @@ block content
br
button(type="submit") #{translate('resetCredentialsLabel', currentLocale.code)}
br
a(href=`${baseUrl}/?space_id=${settings.spaceId}&preview_token=${settings.previewToken}&delivery_token=${settings.deliveryToken}&api=${currentApi.id}${settings.editorialFeatures ? '&enable_editorial_features' : ''}` class="status-block__sharelink") #{translate('copyLinkLabel', currentLocale.code)}
a(href=`${baseUrl}/${queryStringSettings}` class="status-block__sharelink") #{translate('copyLinkLabel', currentLocale.code)}
p
em #{translate('overrideConfigLabel', currentLocale.code)}