From 992c08bc25ba307a014a3a434392d24e7dde36fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Fri, 5 Oct 2018 16:58:20 +0200 Subject: [PATCH 1/2] Install and setup feathers-vuex --- helpers/createApiClient.js | 72 +++++++++++++++++++++++++++++++ nuxt.config.js | 1 + package.json | 1 + plugins/api.js | 69 ++++-------------------------- plugins/init-feathers-vuex.js | 19 +++++++++ store/init-feathers-vuex.js | 13 ++++++ store/services/users.js | 11 +++++ store/services/usersettings.js | 11 +++++ yarn.lock | 78 +++++++++++++++++++++++++--------- 9 files changed, 195 insertions(+), 80 deletions(-) create mode 100644 helpers/createApiClient.js create mode 100644 plugins/init-feathers-vuex.js create mode 100644 store/init-feathers-vuex.js create mode 100644 store/services/users.js create mode 100644 store/services/usersettings.js diff --git a/helpers/createApiClient.js b/helpers/createApiClient.js new file mode 100644 index 00000000..44c8ba0c --- /dev/null +++ b/helpers/createApiClient.js @@ -0,0 +1,72 @@ +import feathers from '@feathersjs/feathers' +import socketio from '@feathersjs/socketio-client' +import io from 'socket.io-client' +import authentication from '@feathersjs/authentication-client' +import urlHelper from '~/helpers/urls' +import Cookie from 'cookie-universal' + +const authKey = 'feathers-jwt' +let socket + +const getSocket = (app) => { + if (socket) return socket + + const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) + if (process.env.ENV === 'production') { + socket = socketio(io(endpoint), { timeout: 20000 }) + if (process.server) { + setTimeout(() => { + // close server connection as content was delivered already after 30 seconds at latest + try { + socket.close() + } catch (err) { + console.log(err) + } + }, 30000) + } + } else { + socket = socketio(io(endpoint)) + } + + return socket +} + +let createApiClient = ({app, req, res}) => { + const cookies = Cookie(req, res) + const storageMapping = { + getItem: (key) => { + const res = cookies.get(key) + // console.log(`## STORAGE: getItem(${key})`, res) + return res + }, + setItem: (key, value, options) => { + const res = cookies.set(key, value, options) + // console.log(`## STORAGE: setItem(${key}, ${value}, ${options})`, res) + return res + }, + removeItem: (key) => { + const res = cookies.remove(key) + // console.log(`## STORAGE: removeItem(${key})`, res) + return res + }, + clear: () => { + const res = cookies.removeAll() + if (process.env.NODE_ENV === 'development') { + console.log(`## STORAGE: clear()`, res) + } + return res + } + } + + let api = feathers() + .configure(getSocket(app)) + .configure(authentication({ + storage: storageMapping, + storageKey: authKey, + cookie: authKey + })) + + return api +} + +export default createApiClient diff --git a/nuxt.config.js b/nuxt.config.js index 7bac7f5d..46131328 100644 --- a/nuxt.config.js +++ b/nuxt.config.js @@ -108,6 +108,7 @@ module.exports = { {src: '~/plugins/debug.js', ssr: false}, {src: '~/plugins/raven-client.js', ssr: false}, {src: '~/plugins/api.js'}, + {src: '~/plugins/init-feathers-vuex.js'}, {src: '~/plugins/vue-directives.js', ssr: false}, {src: '~/plugins/init-store-subscriptions.js', ssr: false}, {src: '~/plugins/keep-alive.js', ssr: false}, diff --git a/package.json b/package.json index 72d45427..bb258cec 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "express-healthcheck": "~0.1.0", "express-locale": "~1.0.5", "express-session": "~1.15.6", + "feathers-vuex": "^1.5.0", "font-awesome": "~4.7.0", "helmet": "^3.12.1", "i18n-iso-countries": "^3.7.8", diff --git a/plugins/api.js b/plugins/api.js index 4619ef7f..6c8a1a81 100644 --- a/plugins/api.js +++ b/plugins/api.js @@ -1,60 +1,15 @@ -import feathers from '@feathersjs/feathers' -import socketio from '@feathersjs/socketio-client' -import io from 'socket.io-client' -import authentication from '@feathersjs/authentication-client' -import urlHelper from '~/helpers/urls' +import feathersVuex from 'feathers-vuex' import Vue from 'vue' +import Vuex from 'vuex' +import createApiClient from '../helpers/createApiClient' -export default ({app, store, redirect, router}) => { - const authKey = 'feathers-jwt' - const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) - const storage = { - getItem: (key) => { - const res = app.$cookies.get(key) - // console.log(`## STORAGE: getItem(${key})`, res) - return res - }, - setItem: (key, value, options) => { - const res = app.$cookies.set(key, value, options) - // console.log(`## STORAGE: setItem(${key}, ${value}, ${options})`, res) - return res - }, - removeItem: (key) => { - const res = app.$cookies.remove(key) - // console.log(`## STORAGE: removeItem(${key})`, res) - return res - }, - clear: () => { - const res = app.$cookies.removeAll() - if (app.$env.NODE_ENV === 'development') { - console.log(`## STORAGE: clear()`, res) - } - return res - } - } +export default ({app, store, redirect, router, req, res}) => { + const api = createApiClient({app, req, res}) + const { FeathersVuex } = feathersVuex(api, { idField: '_id' }) - const socket = io(endpoint) + Vue.use(FeathersVuex) + Vue.use(Vuex) - if (process.server) { - setTimeout(() => { - // close server connection as content was delivered already after 30 seconds at latest - try { - socket.close() - } catch (err) { - app.error(err) - } - }, 30000) - } - - let api = feathers() - .configure(socketio(socket, { - timeout: 20000 - })) - .configure(authentication({ - storage: storage, - storageKey: authKey, - cookie: authKey - })) api.hooks({ before: { all: [ @@ -73,7 +28,7 @@ export default ({app, store, redirect, router}) => { if (app.$env.NODE_ENV === 'development') { console.log('####################') console.error(ctx.error) - console.info('JWT TOKEN: ', app.$cookies.get(authKey)) + // console.info('JWT TOKEN: ', app.$cookies.get(authKey)) console.info('path', ctx.path) console.info('service', ctx.service) console.info('method', ctx.method) @@ -103,7 +58,6 @@ export default ({app, store, redirect, router}) => { // fix issues where we could not log in // when at development await api.passport.logout() - storage.removeItem(authKey) } let user = null const response = await api.authenticate(options) @@ -120,11 +74,6 @@ export default ({app, store, redirect, router}) => { api.channel('authenticated').join(connection) }) - /** - * @deprecated - */ - api.authKey = authKey - // make the api accessible inside vue components Vue.use({ install (Vue) { diff --git a/plugins/init-feathers-vuex.js b/plugins/init-feathers-vuex.js new file mode 100644 index 00000000..4ef7fd81 --- /dev/null +++ b/plugins/init-feathers-vuex.js @@ -0,0 +1,19 @@ +import createApiClient from '../helpers/createApiClient' + +const requireModule = require.context( + // The relative path holding the service modules + '../store/services', + // Whether to look in subfolders + false, + // Only include .js files (prevents duplicate imports) + /.js$/ +) + +export default async ({app, store, req, res}) => { + const feathersClient = createApiClient({app, req, res}) + + const servicePlugins = requireModule.keys().map(modulePath => requireModule(modulePath).default(feathersClient)) + servicePlugins.forEach((servicePlugin) => { + servicePlugin(store) + }) +} diff --git a/store/init-feathers-vuex.js b/store/init-feathers-vuex.js new file mode 100644 index 00000000..4ce4bb78 --- /dev/null +++ b/store/init-feathers-vuex.js @@ -0,0 +1,13 @@ +import { initAuth } from 'feathers-vuex' + +export const actions = { + nuxtServerInit ({ commit, dispatch }, { req }) { + return initAuth({ + commit, + dispatch, + req, + moduleName: 'auth', + cookieName: 'feathers-jwt' + }) + } +} diff --git a/store/services/users.js b/store/services/users.js new file mode 100644 index 00000000..ae200954 --- /dev/null +++ b/store/services/users.js @@ -0,0 +1,11 @@ +import feathersVuex from 'feathers-vuex' + +let servicePlugin = (feathersClient) => { + const { service } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePath = 'users' + const servicePlugin = service(servicePath, { + namespace: 'feathers-vuex-users' + }) + return servicePlugin +} +export default servicePlugin diff --git a/store/services/usersettings.js b/store/services/usersettings.js new file mode 100644 index 00000000..233538fc --- /dev/null +++ b/store/services/usersettings.js @@ -0,0 +1,11 @@ +import feathersVuex from 'feathers-vuex' + +let servicePlugin = (feathersClient) => { + const { service } = feathersVuex(feathersClient, { idField: '_id' }) + const servicePath = 'usersettings' + const servicePlugin = service(servicePath, { + namespace: 'feathers-vuex-usersettings' + }) + return servicePlugin +} +export default servicePlugin diff --git a/yarn.lock b/yarn.lock index 420914e2..54769966 100644 --- a/yarn.lock +++ b/yarn.lock @@ -706,6 +706,12 @@ dependencies: debug "^4.0.0" +"@feathersjs/errors@^3.3.4": + version "3.3.4" + resolved "https://registry.yarnpkg.com/@feathersjs/errors/-/errors-3.3.4.tgz#7dd591626ba57d1e8074464235b4d23906833ca7" + dependencies: + debug "^4.0.0" + "@feathersjs/feathers@3.2.2", "@feathersjs/feathers@^3.1.7": version "3.2.2" resolved "https://registry.yarnpkg.com/@feathersjs/feathers/-/feathers-3.2.2.tgz#026fe3c8a23ad0557d521a533b727b2f209c5918" @@ -3582,7 +3588,7 @@ debug@=3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@^3.0.1, debug@^3.1.0: +debug@^3.0.1, debug@^3.1.0, debug@^3.2.5: version "3.2.5" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" dependencies: @@ -3602,6 +3608,10 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + deep-equal@^1.0.0, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -4621,6 +4631,10 @@ falafel@^2.1.0: isarray "0.0.1" object-keys "^1.0.6" +fast-copy@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-1.2.2.tgz#b15cf14d767f07c22cd5ffbd6cc615c12d206b7e" + fast-deep-equal@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" @@ -4657,6 +4671,23 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" +feathers-vuex@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/feathers-vuex/-/feathers-vuex-1.6.2.tgz#08361839e6ffbb19ec9b56085f56fdec20653e2e" + dependencies: + "@feathersjs/commons" "^3.0.1" + "@feathersjs/errors" "^3.3.4" + debug "^3.2.5" + deep-diff "^1.0.2" + fast-copy "^1.2.2" + inflection "^1.12.0" + jwt-decode "^2.2.0" + lodash.isobject "^3.0.2" + lodash.merge "^4.6.1" + lodash.trim "^4.5.1" + serialize-error "^2.1.0" + sift "^6.0.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -5742,6 +5773,10 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" +inflection@^1.12.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -6541,7 +6576,7 @@ jss@^9.8.1: symbol-observable "^1.1.0" warning "^3.0.0" -jwt-decode@^2.1.0: +jwt-decode@^2.1.0, jwt-decode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-2.2.0.tgz#7d86bd56679f58ce6a84704a657dd392bba81a79" @@ -6653,7 +6688,7 @@ levn@^0.3.0, levn@~0.3.0: dependencies: prelude-ls "~1.1.2" type-check "~0.3.2" - + linkifyjs@^2.1.7: version "2.1.7" resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-2.1.7.tgz#e5d68d2ae30b9c055e1d74cc40f9a31d3abb4012" @@ -6794,6 +6829,10 @@ lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" +lodash.isobject@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d" + lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" @@ -6806,7 +6845,7 @@ lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" -lodash.merge@^4.6.0: +lodash.merge@^4.6.0, lodash.merge@^4.6.1: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" @@ -6835,11 +6874,15 @@ lodash.templatesettings@^4.0.0: dependencies: lodash._reinterpolate "~3.0.0" +lodash.trim@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/lodash.trim/-/lodash.trim-4.5.1.tgz#36425e7ee90be4aa5e27bcebb85b7d11ea47aa57" + lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.11, lodash@~4.17.4: +lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10, lodash@~4.17.11: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -8865,16 +8908,16 @@ promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.6.2: +prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: loose-envify "^1.3.1" object-assign "^4.1.1" -protocol-buffers-schema@^2.0.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-2.2.0.tgz#d29c6cd73fb655978fb6989691180db844119f61" +protocol-buffers-schema@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.3.2.tgz#00434f608b4e8df54c59e070efeefc37fb4bb859" proxy-addr@~2.0.3: version "2.0.4" @@ -9177,7 +9220,7 @@ react-dev-utils@^5.0.0: strip-ansi "3.0.1" text-table "0.2.0" -react-dom@^16.4.2: +react-dom@^16.2.0, react-dom@^16.4.2: version "16.5.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7" dependencies: @@ -9185,7 +9228,7 @@ react-dom@^16.4.2: object-assign "^4.1.1" prop-types "^15.6.2" schedule "^0.5.0" - + react-error-overlay@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89" @@ -9215,15 +9258,6 @@ react@^16.2.0, react@^16.4.2: prop-types "^15.6.2" schedule "^0.5.0" -react@^16.4.2: - version "16.5.2" - resolved "https://registry.yarnpkg.com/react/-/react-16.5.2.tgz#19f6b444ed139baa45609eee6dc3d318b3895d42" - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - schedule "^0.5.0" - read-cache@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" @@ -10087,6 +10121,10 @@ shuffle-seed@^1.1.6: dependencies: seedrandom "^2.4.2" +sift@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/sift/-/sift-6.0.0.tgz#f93a778e5cbf05a5024ebc391e6b32511a6d1f82" + signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" From d7ae96778a3a3fceaa699a2a5f145f6b08e0aa23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 8 Oct 2018 15:17:01 +0200 Subject: [PATCH 2/2] Follow @appinteractive review ``` session on the server side (SSR) is not destroyed which leads to the following scenario: 1. login as admin (test@test.de) 2. logout 3. login as regular user (test3@test3.de) 4. refresh page after you are logged in 5. see that you are logged in again as the last user (admin) instead of your last loggin (user) You need to remove that session also on the server side, maybe there is a cookie still flying around in the browser which is then send on SSR and the backend responds with the logged in user from the token in the cookie. ``` This is not possible now, as the lazy-loading happens with `app.$api` and not the socket. --- helpers/createApiClient.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/helpers/createApiClient.js b/helpers/createApiClient.js index 44c8ba0c..1f29c124 100644 --- a/helpers/createApiClient.js +++ b/helpers/createApiClient.js @@ -6,11 +6,9 @@ import urlHelper from '~/helpers/urls' import Cookie from 'cookie-universal' const authKey = 'feathers-jwt' -let socket - -const getSocket = (app) => { - if (socket) return socket +const createSocket = (app) => { + let socket const endpoint = urlHelper.buildEndpointURL(app.$env.API_HOST, { port: app.$env.API_PORT }) if (process.env.ENV === 'production') { socket = socketio(io(endpoint), { timeout: 20000 }) @@ -32,6 +30,8 @@ const getSocket = (app) => { } let createApiClient = ({app, req, res}) => { + if (app.$api) return app.$api + const cookies = Cookie(req, res) const storageMapping = { getItem: (key) => { @@ -59,7 +59,7 @@ let createApiClient = ({app, req, res}) => { } let api = feathers() - .configure(getSocket(app)) + .configure(createSocket(app)) .configure(authentication({ storage: storageMapping, storageKey: authKey,