diff --git a/app.js b/app.js index 0db24b3..400e6a0 100644 --- a/app.js +++ b/app.js @@ -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,22 +80,46 @@ 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() +})) - const defaultLocale = response.locals.locales - .find((locale) => locale.default) +// 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() - if (request.query.locale) { - response.locals.currentLocale = space.locales - .find((locale) => locale.code === request.query.locale) - } + // Update credentials in cookie when space connection is successful + updateCookie(response, SETTINGS_NAME, settings) - if (!response.locals.currentLocale) { - response.locals.currentLocale = defaultLocale + // Get available locales from space + 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() })) diff --git a/helpers.js b/helpers.js index 30867fe..c6052ce 100644 --- a/helpers.js +++ b/helpers.js @@ -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 diff --git a/lib/settings.js b/lib/settings.js index b5d4a9d..a20396e 100644 --- a/lib/settings.js +++ b/lib/settings.js @@ -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 */ diff --git a/routes/index.js b/routes/index.js index be818e7..c3d82a1 100644 --- a/routes/index.js +++ b/routes/index.js @@ -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)) diff --git a/routes/settings.js b/routes/settings.js index cd3276e..ed1df38 100644 --- a/routes/settings.js +++ b/routes/settings.js @@ -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' - }) + return errorList +} - const settingsQs = querystring.stringify(settingsQuery) - response.locals.queryStringSettings = settingsQs ? `?${settingsQs}` : '' - // Reinit clients - initClients(settings) - } - - // Generate error dictionary - // Format: { FIELD_NAME: [array, of, error, messages] } - const errors = errorList.reduce((errors, error) => { +// Generate error dictionary +// Format: { FIELD_NAME: [array, of, error, messages] } +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) } diff --git a/views/settings.pug b/views/settings.pug index 3bf2c34..8934769 100644 --- a/views/settings.pug +++ b/views/settings.pug @@ -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)}