From f21c8609857f73366814d6fe4cbf51b57092b14b Mon Sep 17 00:00:00 2001 From: Kasper Seweryn Date: Sat, 16 Apr 2022 08:56:26 +0000 Subject: [PATCH] Replace django-channels with `useWebSocket` from `@vueuse/core` (!1759) --- changes/changelog.d/1759.bugfix | 1 + changes/changelog.d/1759.enhancement | 1 + front/package.json | 3 +- front/src/App.vue | 74 +++++++++-------------- front/src/main.js | 2 + front/src/store/auth.js | 17 ------ front/tests/unit/specs/store/auth.spec.js | 26 -------- front/vite.config.js | 8 --- front/yarn.lock | 57 +++++++++-------- 9 files changed, 64 insertions(+), 125 deletions(-) create mode 100644 changes/changelog.d/1759.bugfix create mode 100644 changes/changelog.d/1759.enhancement diff --git a/changes/changelog.d/1759.bugfix b/changes/changelog.d/1759.bugfix new file mode 100644 index 000000000..4389310cf --- /dev/null +++ b/changes/changelog.d/1759.bugfix @@ -0,0 +1 @@ +Fix CSP issue caused by django-channels package (#1752) diff --git a/changes/changelog.d/1759.enhancement b/changes/changelog.d/1759.enhancement new file mode 100644 index 000000000..569282bf5 --- /dev/null +++ b/changes/changelog.d/1759.enhancement @@ -0,0 +1 @@ +Replace django-channels package with web socket implementation from @vueuse/core (#1715) diff --git a/front/package.json b/front/package.json index eef255cd1..d3075cf52 100644 --- a/front/package.json +++ b/front/package.json @@ -17,10 +17,11 @@ "postinstall": "yarn run fix-fomantic-css" }, "dependencies": { + "@vue/composition-api": "1.4.9", + "@vueuse/core": "8.2.5", "axios": "0.26.1", "axios-auth-refresh": "3.2.2", "diff": "5.0.0", - "django-channels": "2.1.3", "focus-trap": "6.7.3", "fomantic-ui-css": "2.8.8", "howler": "2.2.3", diff --git a/front/src/App.vue b/front/src/App.vue index 6bf75fbf2..f35939351 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -50,7 +50,7 @@ import axios from 'axios' import _ from 'lodash' import { mapState, mapGetters } from 'vuex' -import { WebSocketBridge } from 'django-channels' +import { useWebSocket, whenever } from '@vueuse/core' import GlobalEvents from '@/components/utils/global-events.vue' import locales from './locales' import { getClientOnlyRadio } from '@/radios' @@ -65,6 +65,7 @@ import SetInstanceModal from '@/components/SetInstanceModal.vue' import ShortcutsModal from '@/components/ShortcutsModal.vue' import FilterModal from '@/components/moderation/FilterModal.vue' import ReportModal from '@/components/moderation/ReportModal.vue' +import { watch, watchEffect } from '@vue/composition-api' export default { name: 'App', @@ -81,9 +82,32 @@ export default { ReportModal, GlobalEvents }, + setup (props, { root }) { + const store = root.$store + + const url = store.getters['instance/absoluteUrl']('api/v1/activity') + .replace(/^http/, 'ws') + + const { data, status, open, close } = useWebSocket(url, { + autoReconnect: true, + immediate: false + }) + + watch(() => store.state.auth.authenticated, (authenticated) => { + if (authenticated) return open() + close() + }) + + whenever(data, () => { + store.dispatch('ui/websocketEvent', JSON.parse(data.value)) + }) + + watchEffect(() => { + console.log('Websocket status:', status.value) + }) + }, data () { return { - bridge: null, instanceUrl: null, showShortcutsModal: false, showSetInstanceModal: false, @@ -172,13 +196,6 @@ export default { this.setTheme(newValue) } }, - '$store.state.auth.authenticated' (newValue) { - if (!newValue) { - this.disconnect() - } else { - this.openWebsocket() - } - }, '$store.state.ui.currentLanguage': { immediate: true, handler (newValue) { @@ -248,7 +265,6 @@ export default { } window.addEventListener('resize', this.handleResize) this.handleResize() - this.openWebsocket() const self = this if (!this.$store.state.ui.selectedLanguage) { this.autodetectLanguage() @@ -264,7 +280,7 @@ export default { } const url = urlParams.get('_url') if (url) { - this.$router.replace(url) + await this.$router.replace(url) } else if (!this.$store.state.instance.instanceUrl) { // we have several way to guess the API server url. By order of precedence: // 1. use the url provided in settings.json, if any @@ -279,11 +295,6 @@ export default { this.$store.commit('instance/instanceUrl', this.$store.state.instance.instanceUrl) } await this.fetchNodeInfo() - this.$store.dispatch('auth/check') - setInterval(() => { - // used to refresh profile every now and then (important for refreshing scoped tokens) - self.$store.dispatch('auth/check') - }, 1000 * 60 * 60 * 8) this.$store.dispatch('instance/fetchSettings') this.$store.commit('ui/addWebsocketEventHandler', { eventName: 'inbox.item_added', @@ -354,7 +365,6 @@ export default { eventName: 'Listen', id: 'handleListen' }) - this.disconnect() }, methods: { incrementNotificationCountInSidebar (event) { @@ -400,36 +410,6 @@ export default { } this.$store.commit('ui/currentLanguage', candidate) }, - disconnect () { - if (!this.bridge) { - return - } - this.bridge.socket.close(1000, 'goodbye', { keepClosed: true }) - }, - openWebsocket () { - if (!this.$store.state.auth.authenticated) { - return - } - this.disconnect() - const self = this - const token = this.$store.state.auth.token - const bridge = new WebSocketBridge() - this.bridge = bridge - let url = - this.$store.getters['instance/absoluteUrl'](`api/v1/activity?token=${token}`) - url = url.replace('http://', 'ws://') - url = url.replace('https://', 'wss://') - bridge.connect( - url, - [], - { reconnectInterval: 1000 * 60 }) - bridge.addEventListener('message', function (event) { - self.$store.dispatch('ui/websocketEvent', event.data) - }) - bridge.socket.addEventListener('open', function () { - console.log('Connected to WebSocket') - }) - }, getTrackInformationText (track) { const trackTitle = track.title const albumArtist = (track.album) ? track.album.artist.name : null diff --git a/front/src/main.js b/front/src/main.js index 2ff982fc0..23efe39d6 100644 --- a/front/src/main.js +++ b/front/src/main.js @@ -14,6 +14,7 @@ import GetTextPlugin from 'vue-gettext' import { sync } from 'vuex-router-sync' import locales from '@/locales' import createAuthRefreshInterceptor from 'axios-auth-refresh' +import VueCompositionAPI from '@vue/composition-api' import filters from '@/filters' // eslint-disable-line import { parseAPIErrors } from '@/utils' @@ -57,6 +58,7 @@ Vue.use(GetTextPlugin, { silent: true }) +Vue.use(VueCompositionAPI) Vue.use(VueLazyload) Vue.directive('title', function (el, binding) { store.commit('ui/pageTitle', binding.value) diff --git a/front/src/store/auth.js b/front/src/store/auth.js index 5c7530948..8a086180c 100644 --- a/front/src/store/auth.js +++ b/front/src/store/auth.js @@ -54,7 +54,6 @@ export default { moderation: false }, profile: null, - token: '', oauth: getDefaultOauth(), scopedTokens: getDefaultScopedTokens() }, @@ -71,7 +70,6 @@ export default { state.profile = null state.username = '' state.fullUsername = '' - state.token = '' state.scopedTokens = getDefaultScopedTokens() state.oauth = getDefaultOauth() state.availablePermissions = { @@ -89,7 +87,6 @@ export default { if (value === false) { state.username = null state.fullUsername = null - state.token = null state.profile = null state.scopedTokens = getDefaultScopedTokens() state.availablePermissions = {} @@ -106,9 +103,6 @@ export default { state.profile.avatar = value } }, - token: (state, value) => { - state.token = value - }, scopedTokens: (state, value) => { state.scopedTokens = { ...value } }, @@ -138,7 +132,6 @@ export default { }) return axios.post('users/login', form).then(response => { logger.default.info('Successfully logged in as', credentials.username) - // commit('token', response.data.token) dispatch('fetchProfile').then(() => { // Redirect to a specified route import('@/router').then((router) => { @@ -169,16 +162,6 @@ export default { }) logger.default.info('Log out, goodbye!') }, - async check ({ commit, dispatch, state }) { - logger.default.info('Checking authentication…') - commit('authenticated', false) - const profile = await dispatch('fetchProfile') - if (profile) { - commit('authenticated', true) - } else { - logger.default.info('Anonymous user') - } - }, fetchProfile ({ commit, dispatch, state }) { return new Promise((resolve, reject) => { axios.get('users/me/').then((response) => { diff --git a/front/tests/unit/specs/store/auth.spec.js b/front/tests/unit/specs/store/auth.spec.js index 023c253fe..f2f188cd2 100644 --- a/front/tests/unit/specs/store/auth.spec.js +++ b/front/tests/unit/specs/store/auth.spec.js @@ -37,28 +37,15 @@ describe('store/auth', () => { it('authenticated false', () => { const state = { username: 'dummy', - token: 'dummy', profile: 'dummy', availablePermissions: 'dummy' } store.mutations.authenticated(state, false) expect(state.authenticated).to.equal(false) expect(state.username).to.equal(null) - expect(state.token).to.equal(null) expect(state.profile).to.equal(null) expect(state.availablePermissions).to.deep.equal({}) }) - it('token null', () => { - const state = {} - store.mutations.token(state, null) - expect(state.token).to.equal(null) - }) - it('token real', () => { - const state = {} - let token = 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTUxNTUzMzQyOSwiZXhwIjoxNTE1NTM3MDI5LCJpYXQiOjE1MTU1MzM0MjksImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.' - store.mutations.token(state, token) - expect(state.token).to.equal(token) - }) it('permissions', () => { const state = { availablePermissions: {} } store.mutations.permission(state, {key: 'admin', status: true}) @@ -86,19 +73,6 @@ describe('store/auth', () => { ] }) }) - it('check jwt null', () => { - testAction({ - action: store.actions.check, - params: {state: {}}, - expectedMutations: [ - { type: 'authenticated', payload: false }, - { type: 'authenticated', payload: true }, - ], - expectedActions: [ - { type: 'fetchProfile' }, - ] - }) - }) it('login success', () => { moxios.stubRequest('token/', { status: 200, diff --git a/front/vite.config.js b/front/vite.config.js index 82e3ba06e..549890e89 100644 --- a/front/vite.config.js +++ b/front/vite.config.js @@ -17,14 +17,6 @@ export default defineConfig({ } } }, - { - name: 'fix-django-channels', - transform (src, id) { - if (id.includes('django-channels')) { - return `var parcelRequire;${src}` - } - } - } ], server: { port: process.env.VUE_PORT || '8080', diff --git a/front/yarn.lock b/front/yarn.lock index f168a642b..8a5a11c8c 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1009,13 +1009,6 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" -"@babel/runtime@^7.5.5": - version "7.17.7" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.7.tgz#a5f3328dc41ff39d803f311cfe17703418cf9825" - integrity sha512-L6rvG9GDxaLgFjg41K+5Yv9OMrU98sWe+Ykmc6FDJW/+vYZMhdOMKkISgzptMaERHvS2Y2lw9MDRm2gHhlQQoA== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/runtime@^7.8.4": version "7.17.8" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" @@ -1603,6 +1596,11 @@ optionalDependencies: prettier "^1.18.2 || ^2.0.0" +"@vue/composition-api@^1.4.9": + version "1.4.9" + resolved "https://registry.yarnpkg.com/@vue/composition-api/-/composition-api-1.4.9.tgz#6fa65284f545887b52d421f23b4fa1c41bc0ad4b" + integrity sha512-l6YOeg5LEXmfPqyxAnBaCv1FMRw0OGKJ4m6nOWRm6ngt5TuHcj5ZoBRN+LXh3J0u6Ur3C4VA+RiKT+M0eItr/g== + "@vue/reactivity-transform@3.2.31": version "3.2.31" resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.31.tgz#0f5b25c24e70edab2b613d5305c465b50fc00911" @@ -1628,6 +1626,27 @@ lodash "^4.17.15" pretty "^2.0.0" +"@vueuse/core@^8.2.5": + version "8.2.5" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-8.2.5.tgz#ca6a59091ecf16e6739c53f3d857b11967a5eb06" + integrity sha512-5prZAA1Ji2ltwNUnzreu6WIXYqHYP/9U2BiY5mD/650VYLpVcwVlYznJDFcLCmEWI3o3Vd34oS1FUf+6Mh68GQ== + dependencies: + "@vueuse/metadata" "8.2.5" + "@vueuse/shared" "8.2.5" + vue-demi "*" + +"@vueuse/metadata@8.2.5": + version "8.2.5" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-8.2.5.tgz#51c7d95e04284ea378a5242a2e88b77494e2c117" + integrity sha512-Lk9plJjh9cIdiRdcj16dau+2LANxIdFCiTgdfzwYXbflxq0QnMBeOD2qHgKDE7fuVrtPcVWj8VSuZEx1HRfNQA== + +"@vueuse/shared@8.2.5": + version "8.2.5" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-8.2.5.tgz#1ae200a240c4b8d42d41723b64d8f917aa57ff16" + integrity sha512-lNWo+7sk6JCuOj4AiYM+6HZ6fq4xAuVq1sVckMQKgfCJZpZRe4i8es+ZULO5bYTKP+VrOCtqrLR2GzEfrbr3YQ== + dependencies: + vue-demi "*" + abab@^2.0.3, abab@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a" @@ -2572,15 +2591,6 @@ diff@5.0.0, diff@^5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -django-channels@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/django-channels/-/django-channels-2.1.3.tgz#4d175b9d8553f3e2b1263b75de0b4f23fc9acac3" - integrity sha512-H0jzdw3XvBR3HC4FqMqhoM0M4iUlOJ1TCRpyL51r//8LX2KGAK7lA1+4JeTxvnlaq8xBvZ3mMTf55eaRoDcgKA== - dependencies: - "@babel/runtime" "^7.5.5" - event-target-shim "^5.0.1" - reconnecting-websocket "^4.1.10" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3121,11 +3131,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -event-target-shim@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -5136,11 +5141,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reconnecting-websocket@^4.1.10: - version "4.4.0" - resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783" - integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng== - regenerate-unicode-properties@^10.0.1: version "10.0.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz#7f442732aa7934a3740c779bb9b3340dccc1fb56" @@ -5849,6 +5849,11 @@ void-elements@^3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk= +vue-demi@*: + version "0.12.5" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.12.5.tgz#8eeed566a7d86eb090209a11723f887d28aeb2d1" + integrity sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q== + vue-eslint-parser@^7.10.0: version "7.11.0" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz#214b5dea961007fcffb2ee65b8912307628d0daf"