diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66fc1c9f5..a28680279 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -441,7 +441,7 @@ jobs: report_name: Coverage Admin Interface type: lcov result_path: ./coverage/lcov.info - min_coverage: 52 + min_coverage: 53 token: ${{ github.token }} ############################################################################## diff --git a/admin/src/graphql/verifyLogin.js b/admin/src/graphql/verifyLogin.js index 59f5e7eb1..553557f3c 100644 --- a/admin/src/graphql/verifyLogin.js +++ b/admin/src/graphql/verifyLogin.js @@ -5,6 +5,7 @@ export const verifyLogin = gql` verifyLogin { firstName lastName + isAdmin id } } diff --git a/admin/src/main.js b/admin/src/main.js index e6f5a80e1..6a59f1cc7 100644 --- a/admin/src/main.js +++ b/admin/src/main.js @@ -75,7 +75,7 @@ Vue.use(Toasted, { }, }) -addNavigationGuards(router, store) +addNavigationGuards(router, store, apolloProvider.defaultClient) new Vue({ moment, diff --git a/admin/src/router/guards.js b/admin/src/router/guards.js index d59234a25..4ed6c8516 100644 --- a/admin/src/router/guards.js +++ b/admin/src/router/guards.js @@ -1,12 +1,28 @@ +import { verifyLogin } from '../graphql/verifyLogin' import CONFIG from '../config' -const addNavigationGuards = (router, store) => { +const addNavigationGuards = (router, store, apollo) => { // store token on `authenticate` - router.beforeEach((to, from, next) => { + router.beforeEach(async (to, from, next) => { if (to.path === '/authenticate' && to.query && to.query.token) { - // TODO verify user to get user data store.commit('token', to.query.token) - next({ path: '/' }) + await apollo + .query({ + query: verifyLogin, + fetchPolicy: 'network-only', + }) + .then((result) => { + const moderator = result.data.verifyLogin + if (moderator.isAdmin) { + store.commit('moderator', moderator) + next({ path: '/' }) + } else { + next({ path: '/not-found' }) + } + }) + .catch(() => { + next({ path: '/not-found' }) + }) } else { next() } @@ -16,7 +32,9 @@ const addNavigationGuards = (router, store) => { router.beforeEach((to, from, next) => { if ( !CONFIG.DEBUG_DISABLE_AUTH && // we did not disabled the auth module for debug purposes - !store.state.token && // we do not have a token + (!store.state.token || // we do not have a token + !store.state.moderator || // no moderator set in store + !store.state.moderator.isAdmin) && // user is no admin to.path !== '/not-found' && // we are not on `not-found` to.path !== '/logout' // we are not on `logout` ) { diff --git a/admin/src/router/guards.test.js b/admin/src/router/guards.test.js index e69846aab..cd5b33e68 100644 --- a/admin/src/router/guards.test.js +++ b/admin/src/router/guards.test.js @@ -2,6 +2,13 @@ import addNavigationGuards from './guards' import router from './router' const storeCommitMock = jest.fn() +const apolloQueryMock = jest.fn().mockResolvedValue({ + data: { + verifyLogin: { + isAdmin: true, + }, + }, +}) const store = { commit: storeCommitMock, @@ -10,7 +17,11 @@ const store = { }, } -addNavigationGuards(router, store) +const apollo = { + query: apolloQueryMock, +} + +addNavigationGuards(router, store, apollo) describe('navigation guards', () => { beforeEach(() => { @@ -21,18 +32,70 @@ describe('navigation guards', () => { const navGuard = router.beforeHooks[0] const next = jest.fn() - describe('with valid token', () => { - it('commits the token to the store', async () => { + describe('with valid token and as admin', () => { + beforeEach(() => { navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next) + }) + + it('commits the token to the store', async () => { expect(storeCommitMock).toBeCalledWith('token', 'valid-token') }) + it('commits the moderator to the store', () => { + expect(storeCommitMock).toBeCalledWith('moderator', { isAdmin: true }) + }) + it('redirects to /', async () => { - navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next) expect(next).toBeCalledWith({ path: '/' }) }) }) + describe('with valid token and not as admin', () => { + beforeEach(() => { + apolloQueryMock.mockResolvedValue({ + data: { + verifyLogin: { + isAdmin: false, + }, + }, + }) + navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next) + }) + + it('commits the token to the store', async () => { + expect(storeCommitMock).toBeCalledWith('token', 'valid-token') + }) + + it('does not commit the moderator to the store', () => { + expect(storeCommitMock).not.toBeCalledWith('moderator', { isAdmin: false }) + }) + + it('redirects to /not-found', async () => { + expect(next).toBeCalledWith({ path: '/not-found' }) + }) + }) + + describe('with valid token and server error on verification', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ + message: 'Ouch!', + }) + navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next) + }) + + it('commits the token to the store', async () => { + expect(storeCommitMock).toBeCalledWith('token', 'valid-token') + }) + + it('does not commit the moderator to the store', () => { + expect(storeCommitMock).not.toBeCalledWith('moderator', { isAdmin: false }) + }) + + it('redirects to /not-found', async () => { + expect(next).toBeCalledWith({ path: '/not-found' }) + }) + }) + describe('without valid token', () => { it('does not commit the token to the store', async () => { navGuard({ path: '/authenticate' }, {}, next) @@ -55,9 +118,16 @@ describe('navigation guards', () => { expect(next).toBeCalledWith({ path: '/not-found' }) }) - it('does not redirect when token in store', () => { + it('redirects to not found with token in store and not moderator', () => { store.state.token = 'valid token' navGuard({ path: '/' }, {}, next) + expect(next).toBeCalledWith({ path: '/not-found' }) + }) + + it('does not redirect with token in store and as moderator', () => { + store.state.token = 'valid token' + store.state.moderator = { isAdmin: true } + navGuard({ path: '/' }, {}, next) expect(next).toBeCalledWith() }) }) diff --git a/admin/src/store/store.js b/admin/src/store/store.js index 9319eafd8..fe5629e19 100644 --- a/admin/src/store/store.js +++ b/admin/src/store/store.js @@ -29,6 +29,7 @@ export const mutations = { export const actions = { logout: ({ commit, state }) => { commit('token', null) + commit('moderator', null) window.localStorage.clear() }, } @@ -41,7 +42,7 @@ const store = new Vuex.Store({ ], state: { token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null, - moderator: { name: 'Dertest Moderator', id: 0 }, + moderator: null, openCreations: 0, }, // Syncronous mutation of the state diff --git a/admin/src/store/store.test.js b/admin/src/store/store.test.js index 7b8784cdd..e027ebf1a 100644 --- a/admin/src/store/store.test.js +++ b/admin/src/store/store.test.js @@ -3,8 +3,14 @@ import CONFIG from '../config' jest.mock('../config') -const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations, setOpenCreations } = - mutations +const { + token, + openCreationsPlus, + openCreationsMinus, + resetOpenCreations, + setOpenCreations, + moderator, +} = mutations const { logout } = actions CONFIG.DEBUG_DISABLE_AUTH = true @@ -43,6 +49,14 @@ describe('Vuex store', () => { }) }) + describe('moderator', () => { + it('sets the moderator object in state', () => { + const state = { moderator: null } + moderator(state, { id: 1 }) + expect(state.moderator).toEqual({ id: 1 }) + }) + }) + describe('setOpenCreations', () => { it('sets the open creations to given value', () => { const state = { openCreations: 24 } @@ -67,6 +81,11 @@ describe('Vuex store', () => { expect(commit).toBeCalledWith('token', null) }) + it('deletes the moderator in store', () => { + logout({ commit, state }) + expect(commit).toBeCalledWith('moderator', null) + }) + it.skip('clears the window local storage', () => { expect(windowStorageMock).toBeCalled() })