feat(query-params): proper handling in case of invalid credentials
This commit is contained in:
committed by
Benedikt Rötsch
parent
ff305e7d34
commit
46074b508d
53
app.js
53
app.js
@@ -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,12 +80,24 @@ 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
|
}))
|
||||||
|
|
||||||
|
// 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
|
const defaultLocale = response.locals.locales
|
||||||
.find((locale) => locale.default)
|
.find((locale) => locale.default)
|
||||||
|
|
||||||
@@ -110,6 +109,18 @@ app.use(catchErrors(async function (request, response, next) {
|
|||||||
if (!response.locals.currentLocale) {
|
if (!response.locals.currentLocale) {
|
||||||
response.locals.currentLocale = defaultLocale
|
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()
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
36
helpers.js
36
helpers.js
@@ -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
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
response.locals.queryStringSettings = settingsQs ? `?${settingsQs}` : ''
|
|
||||||
// Reinit clients
|
|
||||||
initClients(settings)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate error dictionary
|
// Generate error dictionary
|
||||||
// Format: { FIELD_NAME: [array, of, error, messages] }
|
// Format: { FIELD_NAME: [array, of, error, messages] }
|
||||||
const errors = errorList.reduce((errors, error) => {
|
function generateErrorDictionary (errorList) {
|
||||||
|
return 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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user