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

69
app.js
View File

@@ -13,11 +13,14 @@ require('dotenv').config({ path: 'variables.env' })
const helpers = require('./helpers') const helpers = require('./helpers')
const { translate, initializeTranslations } = require('./i18n/i18n') const { translate, initializeTranslations } = require('./i18n/i18n')
const breadcrumb = require('./lib/breadcrumb') const breadcrumb = require('./lib/breadcrumb')
const { updateCookie } = require('./lib/cookies')
const settings = require('./lib/settings') const settings = require('./lib/settings')
const routes = require('./routes/index') const routes = require('./routes/index')
const { getSpace } = require('./services/contentful') const { getSpace } = require('./services/contentful')
const { catchErrors } = require('./handlers/errorHandlers') const { catchErrors } = require('./handlers/errorHandlers')
const SETTINGS_NAME = 'theExampleAppSettings'
const app = express() const app = express()
// View engine setup // View engine setup
@@ -53,27 +56,11 @@ app.use(catchErrors(async function (request, response, next) {
response.locals.helpers = helpers response.locals.helpers = helpers
// Make query string available in templates to render links properly // Make query string available in templates to render links properly
const qs = querystring.stringify(request.query) const cleanQuery = helpers.cleanupQueryParameters(request.query)
// Creates a query string which adds the current credentials to links const qs = querystring.stringify(cleanQuery)
// 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 settingsQs = querystring.stringify(settingsQuery)
response.locals.queryString = qs ? `?${qs}` : '' response.locals.queryString = qs ? `?${qs}` : ''
response.locals.queryStringSettings = settingsQs ? `?${settingsQs}` : '' response.locals.queryStringSettings = response.locals.queryString
response.locals.query = request.query response.locals.query = request.query
response.locals.currentPath = request.path response.locals.currentPath = request.path
@@ -93,22 +80,46 @@ app.use(catchErrors(async function (request, response, next) {
} }
] ]
// Set currently used api
response.locals.currentApi = apis response.locals.currentApi = apis
.find((api) => api.id === (request.query.api || 'cda')) .find((api) => api.id === (request.query.api || 'cda'))
const space = await getSpace() next()
response.locals.locales = space.locales }))
const defaultLocale = response.locals.locales // Test space connection and attach space related data for views if possible
.find((locale) => locale.default) app.use(catchErrors(async function (request, response, next) {
// Catch misconfigured space credentials and display settings page
try {
const space = await getSpace()
if (request.query.locale) { // Update credentials in cookie when space connection is successful
response.locals.currentLocale = space.locales updateCookie(response, SETTINGS_NAME, settings)
.find((locale) => locale.code === request.query.locale)
}
if (!response.locals.currentLocale) { // Get available locales from space
response.locals.currentLocale = defaultLocale response.locals.locales = space.locales
const defaultLocale = response.locals.locales
.find((locale) => locale.default)
if (request.query.locale) {
response.locals.currentLocale = space.locales
.find((locale) => locale.code === request.query.locale)
}
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() next()
})) }))

View File

@@ -1,4 +1,6 @@
const marked = require('marked') const marked = require('marked')
const querystring = require('querystring')
const { translate } = require('./i18n/i18n') const { translate } = require('./i18n/i18n')
// Parse markdown text // 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)}` 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 spaceId = process.env.CONTENTFUL_SPACE_ID
const deliveryToken = process.env.CONTENTFUL_DELIVERY_TOKEN const deliveryToken = process.env.CONTENTFUL_DELIVERY_TOKEN
const previewToken = process.env.CONTENTFUL_PREVIEW_TOKEN const previewToken = process.env.CONTENTFUL_PREVIEW_TOKEN
@@ -29,6 +31,38 @@ module.exports.isCustomCredentials = (settings) => {
settings.previewToken !== previewToken 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 * Evil users might try to add base64 url data to execute js code
* so we should purge any potentially harmful data to mitigate risk * so we should purge any potentially harmful data to mitigate risk

View File

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

View File

@@ -12,6 +12,15 @@ const { getSettings, postSettings } = require('./settings')
const { getLandingPage } = require('./landingPage') const { getLandingPage } = require('./landingPage')
const { getImprint } = require('./imprint') 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 // GET the home landing page
router.get('/', catchErrors(getLandingPage)) router.get('/', catchErrors(getLandingPage))

View File

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