diff --git a/CHANGELOG.md b/CHANGELOG.md index 5460ee90ed..49ef1c2189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **IMPORTANT** for security reasons we added new config `users.allowModification`. + This can help to dissallow modifying fields that shouldn't be changed by user. +- Add helmet - enabled by default, you can pass configuration by adding `config.server.helmet.config`. + More info about helmet configuration https://helmetjs.github.io/docs/ +- Add config `users.tokenInHeader` which allows to send token in header instead in query. Require to set on true same config in vsf-api. + ### Fixed - remove deprecated value from attributesListQuery query - @gibkigonzo (#4572) - Fixed dutch translations - @1070rik (#4587) +- localForage memory overload fixed. `localForage.preserveCollections` keeps names of collections to be preserved from being cleared. - @prakowski ### Changed / Improved diff --git a/config/default.json b/config/default.json index 52644d2a10..c89c55cc31 100644 --- a/config/default.json +++ b/config/default.json @@ -66,6 +66,9 @@ "trace": { "enabled": false, "config": {} + }, + "helmet": { + "enabled": true } }, "initialResources": [ @@ -702,7 +705,11 @@ "syncTasks": "LOCALSTORAGE", "ordersHistory": "LOCALSTORAGE", "checkout": "LOCALSTORAGE" - } + }, + "preserveCollections": [ + "cart", + "user" + ] }, "reviews": { "create_endpoint": "/api/review/create" @@ -718,7 +725,9 @@ "login_endpoint": "/api/user/login", "create_endpoint": "/api/user/create", "me_endpoint": "/api/user/me?token={{token}}", - "refresh_endpoint": "/api/user/refresh" + "refresh_endpoint": "/api/user/refresh", + "allowModification": ["firstname", "lastname", "email", "addresses"], + "tokenInHeader": false }, "stock": { "synchronize": true, diff --git a/core/data-resolver/ProductService.ts b/core/data-resolver/ProductService.ts index 2b839e8fc1..3dd1622d38 100644 --- a/core/data-resolver/ProductService.ts +++ b/core/data-resolver/ProductService.ts @@ -104,7 +104,7 @@ const getProductRenderList = async ({ url = `${url}&userGroupId=${userGroupId}` } - if (token) { + if (token && !config.users.tokenInHeader) { url = `${url}&token=${token}` } @@ -112,7 +112,10 @@ const getProductRenderList = async ({ const task = await TaskQueue.execute({ url, // sync the cart payload: { method: 'GET', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...(token && config.users.tokenInHeader ? { authorization: `Bearer ${token}` } : {}) + }, mode: 'cors' }, callback_event: 'prices-after-sync' diff --git a/core/lib/storage-manager.ts b/core/lib/storage-manager.ts index 7619f81b1a..23b2c1e635 100644 --- a/core/lib/storage-manager.ts +++ b/core/lib/storage-manager.ts @@ -59,6 +59,14 @@ const StorageManager = { } else { return this.storageMap[collectionName] } + }, + clear (): Promise { + const promiseArray = Object.keys(this.storageMap).map((collectionName) => { + return (config.localForage.preserveCollections || []).every(collectionToKeep => collectionName !== collectionToKeep) && this.storageMap[collectionName].clear().then(() => { + Logger.warn(`storeManager cleared: ${collectionName}`, `storeManager cleared: ${collectionName}`, `storeManager cleared: ${collectionName}`)() + }) + }) + return Promise.all(promiseArray) } } diff --git a/core/lib/store/storage.ts b/core/lib/store/storage.ts index 042ebb0c25..792c5d7c6f 100644 --- a/core/lib/store/storage.ts +++ b/core/lib/store/storage.ts @@ -2,6 +2,7 @@ import * as localForage from 'localforage' import { Logger } from '@vue-storefront/core/lib/logger' import { isServer } from '@vue-storefront/core/helpers' import cloneDeep from 'lodash-es/cloneDeep' +import { StorageManager } from '@vue-storefront/core/lib/storage-manager' const CACHE_TIMEOUT = 800 const CACHE_TIMEOUT_ITERATE = 2000 @@ -334,16 +335,23 @@ class LocalForageCacheDriver { }) } else { let isResolved = false - const promise = this._localForageCollection.ready().then(() => this._localForageCollection.setItem(key, copiedValue).then(result => { - if (isCallbackCallable) { - callback(null, result) - } - isResolved = true - }).catch(err => { - isResolved = true - this._lastError = err - throw err - })) + const handleSetItem = () => this._localForageCollection.setItem(key, copiedValue) + .then(result => { + if (isCallbackCallable) { + callback(null, result) + } + isResolved = true + }) + .catch(async err => { + if (err.name === 'QuotaExceededError' || err.name === 'NS_ERROR_DOM_QUOTA_REACHED') { + await StorageManager.clear() + handleSetItem() + } + isResolved = true + this._lastError = err + throw err + }) + const promise = this._localForageCollection.ready().then(handleSetItem) clearTimeout(this._cacheTimeouts.iterate) this._cacheTimeouts.setItem = setTimeout(() => { if (!isResolved) { // this is cache time out check diff --git a/core/lib/sync/task.ts b/core/lib/sync/task.ts index 7fe74eb26f..85f13978f4 100644 --- a/core/lib/sync/task.ts +++ b/core/lib/sync/task.ts @@ -15,6 +15,7 @@ import { serial } from '@vue-storefront/core/helpers' import config from 'config' import { onlineHelper } from '@vue-storefront/core/helpers' import { hasResponseError, getResponseMessage } from '@vue-storefront/core/lib/sync/helpers' +import queryString from 'query-string' export function _prepareTask (task) { const taskId = entities.uniqueEntityId(task) // timestamp as a order id is not the best we can do but it's enough @@ -29,6 +30,36 @@ function _sleep (time) { return new Promise((resolve) => setTimeout(resolve, time)) } +function getUrl (task, currentToken, currentCartId) { + let url = task.url + .replace('{{token}}', (currentToken == null) ? '' : currentToken) + .replace('{{cartId}}', (currentCartId == null) ? '' : currentCartId) + + url = processURLAddress(url); // use relative url paths + if (config.storeViews.multistore) { + url = adjustMultistoreApiUrl(url) + } + + if (config.users.tokenInHeader) { + const parsedUrl = queryString.parseUrl(url) + delete parsedUrl['query']['token'] + url = queryString.stringifyUrl(parsedUrl) + } + + return url +} + +function getPayload (task, currentToken) { + const payload = { + ...task.payload, + headers: { + ...task.payload.headers, + ...(config.users.tokenInHeader ? { authorization: `Bearer ${currentToken}` } : {}) + } + } + return payload +} + function _internalExecute (resolve, reject, task: Task, currentToken, currentCartId) { if (currentToken && rootStore.state.userTokenInvalidateLock > 0) { // invalidate lock set Logger.log('Waiting for rootStore.state.userTokenInvalidateLock to release for ' + task.url, 'sync')() @@ -52,14 +83,11 @@ function _internalExecute (resolve, reject, task: Task, currentToken, currentCar reject('Error executing sync task ' + task.url + ' the required cartId argument is null. Re-creating shopping cart synchro.') return } - let url = task.url.replace('{{token}}', (currentToken == null) ? '' : currentToken).replace('{{cartId}}', (currentCartId == null) ? '' : currentCartId) - url = processURLAddress(url); // use relative url paths - if (config.storeViews.multistore) { - url = adjustMultistoreApiUrl(url) - } + const url = getUrl(task, currentToken, currentCartId) + const payload = getPayload(task, currentToken) let silentMode = false Logger.info('Executing sync task ' + url, 'sync', task)() - return fetch(url, task.payload).then((response) => { + return fetch(url, payload).then((response) => { const contentType = response.headers.get('content-type') if (contentType && contentType.includes('application/json')) { return response.json() diff --git a/core/modules/cart/test/unit/helpers/cartCacheHandler.spec.ts b/core/modules/cart/test/unit/helpers/cartCacheHandler.spec.ts index b6a50c1852..f57bc3edeb 100644 --- a/core/modules/cart/test/unit/helpers/cartCacheHandler.spec.ts +++ b/core/modules/cart/test/unit/helpers/cartCacheHandler.spec.ts @@ -10,6 +10,11 @@ const StorageManager = { }, get (key) { return this[key] + }, + clear () { + return new Promise((resolve, reject) => { + resolve() + }) } }; const cartCacheHandlerFactory = require('../../../helpers/cartCacheHandler').cartCacheHandlerFactory diff --git a/core/modules/catalog/helpers/search.ts b/core/modules/catalog/helpers/search.ts index c35d70c8b9..2be2cf5aac 100644 --- a/core/modules/catalog/helpers/search.ts +++ b/core/modules/catalog/helpers/search.ts @@ -45,15 +45,7 @@ export const storeProductToCache = (product, cacheByKey) => { const cacheKey = getCacheKey(product, cacheByKey); const cache = StorageManager.get('elasticCache'); - cache - .setItem(cacheKey, product, null, config.products.disablePersistentProductsCache) - .catch(err => { - Logger.error('Cannot store cache for ' + cacheKey, err)(); - if (err.name === 'QuotaExceededError' || err.name === 'NS_ERROR_DOM_QUOTA_REACHED') { - // quota exceeded error - cache.clear(); // clear products cache if quota exceeded - } - }); + cache.setItem(cacheKey, product, null, config.products.disablePersistentProductsCache) }; export const preConfigureProduct = ({ product, populateRequestCacheTags }) => { diff --git a/core/modules/user/components/UserAccount.ts b/core/modules/user/components/UserAccount.ts index b62cad6310..a82dbb8b84 100644 --- a/core/modules/user/components/UserAccount.ts +++ b/core/modules/user/components/UserAccount.ts @@ -1,4 +1,6 @@ import toString from 'lodash-es/toString' +import pick from 'lodash-es/pick' +import config from 'config' const Countries = require('@vue-storefront/core/i18n/resource/countries.json') export const UserAccount = { @@ -86,7 +88,7 @@ export const UserAccount = { !this.objectsEqual(this.userCompany, this.getUserCompany()) || (this.userCompany.company && !this.addCompany) ) { - updatedProfile = JSON.parse(JSON.stringify(this.$store.state.user.current)) + updatedProfile = pick(JSON.parse(JSON.stringify(this.$store.state.user.current)), [...config.users.allowModification, 'default_billing']) updatedProfile.firstname = this.currentUser.firstname updatedProfile.lastname = this.currentUser.lastname updatedProfile.email = this.currentUser.email diff --git a/core/modules/user/components/UserShippingDetails.ts b/core/modules/user/components/UserShippingDetails.ts index 8f9eeaa8b9..87671b5fde 100644 --- a/core/modules/user/components/UserShippingDetails.ts +++ b/core/modules/user/components/UserShippingDetails.ts @@ -1,4 +1,6 @@ import toString from 'lodash-es/toString' +import pick from 'lodash-es/pick' +import config from 'config' const Countries = require('@vue-storefront/i18n/resource/countries.json') export const UserShippingDetails = { @@ -80,7 +82,7 @@ export const UserShippingDetails = { updateDetails () { let updatedShippingDetails if (!this.objectsEqual(this.shippingDetails, this.getShippingDetails())) { - updatedShippingDetails = JSON.parse(JSON.stringify(this.$store.state.user.current)) + updatedShippingDetails = pick(JSON.parse(JSON.stringify(this.$store.state.user.current)), config.users.allowModification) let updatedShippingDetailsAddress = { firstname: this.shippingDetails.firstName, lastname: this.shippingDetails.lastName, diff --git a/core/package.json b/core/package.json index faf03dfa35..6c46a332ff 100644 --- a/core/package.json +++ b/core/package.json @@ -12,6 +12,7 @@ "compression": "^1.7.4", "config": "^1.30.0", "express": "^4.14.0", + "helmet": "^3.23.3", "html-minifier": "^4.0.0", "lean-he": "^2.0.0", "localforage": "^1.7.2", diff --git a/core/scripts/server.ts b/core/scripts/server.ts index ddf44f6670..1dc1394777 100755 --- a/core/scripts/server.ts +++ b/core/scripts/server.ts @@ -20,6 +20,7 @@ serverHooksExecutors.afterProcessStarted(config.server) const express = require('express') const ms = require('ms') const request = require('request'); +const helmet = require('helmet') const cache = require('./utils/cache-instance') const apiStatus = require('./utils/api-status') @@ -137,6 +138,10 @@ const serve = (path, cache, options?) => express.static(resolve(path), Object.as const themeRoot = require('../build/theme-path') +if (config.server.helmet && config.server.helmet.enabled && isProd) { + app.use(helmet(config.server.helmet.config)) +} + app.use('/dist', serve('dist', true)) app.use('/assets', serve(themeRoot + '/assets', true)) app.use('/service-worker.js', serve('dist/service-worker.js', false, {