diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff6f4e831..3d58752e7 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: 47 + min_coverage: 51 token: ${{ github.token }} ############################################################################## diff --git a/admin/src/App.spec.js b/admin/src/App.spec.js index e77bc578b..6936394f1 100644 --- a/admin/src/App.spec.js +++ b/admin/src/App.spec.js @@ -7,11 +7,19 @@ const stubs = { RouterView: true, } +const mocks = { + $store: { + state: { + token: null, + }, + }, +} + describe('App', () => { let wrapper const Wrapper = () => { - return shallowMount(App, { localVue, stubs }) + return shallowMount(App, { localVue, stubs, mocks }) } describe('shallowMount', () => { diff --git a/admin/src/router/guards.js b/admin/src/router/guards.js index f6d8058aa..d59234a25 100644 --- a/admin/src/router/guards.js +++ b/admin/src/router/guards.js @@ -3,7 +3,7 @@ import CONFIG from '../config' const addNavigationGuards = (router, store) => { // store token on `authenticate` router.beforeEach((to, from, next) => { - if (to.path === '/authenticate' && to.query.token) { + if (to.path === '/authenticate' && to.query && to.query.token) { // TODO verify user to get user data store.commit('token', to.query.token) next({ path: '/' }) diff --git a/admin/src/router/guards.test.js b/admin/src/router/guards.test.js new file mode 100644 index 000000000..e69846aab --- /dev/null +++ b/admin/src/router/guards.test.js @@ -0,0 +1,64 @@ +import addNavigationGuards from './guards' +import router from './router' + +const storeCommitMock = jest.fn() + +const store = { + commit: storeCommitMock, + state: { + token: null, + }, +} + +addNavigationGuards(router, store) + +describe('navigation guards', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('authenticate', () => { + const navGuard = router.beforeHooks[0] + const next = jest.fn() + + describe('with valid token', () => { + it('commits the token to the store', async () => { + navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next) + expect(storeCommitMock).toBeCalledWith('token', 'valid-token') + }) + + it('redirects to /', async () => { + navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next) + expect(next).toBeCalledWith({ path: '/' }) + }) + }) + + describe('without valid token', () => { + it('does not commit the token to the store', async () => { + navGuard({ path: '/authenticate' }, {}, next) + expect(storeCommitMock).not.toBeCalledWith() + }) + + it('calls next withou arguments', async () => { + navGuard({ path: '/authenticate' }, {}, next) + expect(next).toBeCalledWith() + }) + }) + }) + + describe('protect all routes', () => { + const navGuard = router.beforeHooks[1] + const next = jest.fn() + + it('redirects no not found with no token in store ', () => { + navGuard({ path: '/' }, {}, next) + expect(next).toBeCalledWith({ path: '/not-found' }) + }) + + it('does not redirect when token in store', () => { + store.state.token = 'valid token' + navGuard({ path: '/' }, {}, next) + expect(next).toBeCalledWith() + }) + }) +}) diff --git a/admin/src/router/router.test.js b/admin/src/router/router.test.js index 8e2e70d4d..eb9b646cb 100644 --- a/admin/src/router/router.test.js +++ b/admin/src/router/router.test.js @@ -44,19 +44,19 @@ describe('router', () => { }) describe('routes', () => { + it('has seven routes defined', () => { + expect(routes).toHaveLength(7) + }) + it('has "/overview" as default', async () => { const component = await routes.find((r) => r.path === '/').component() expect(component.default.name).toBe('overview') }) - it('has fourteen routes defined', () => { - expect(routes).toHaveLength(6) - }) - - describe('overview', () => { - it('loads the "Overview" component', async () => { - const component = await routes.find((r) => r.path === '/overview').component() - expect(component.default.name).toBe('overview') + describe('logout', () => { + it('loads the "NotFoundPage" component', async () => { + const component = await routes.find((r) => r.path === '/logout').component() + expect(component.default.name).toBe('not-found') }) }) diff --git a/admin/src/store/store.js b/admin/src/store/store.js index 5f8b4249d..754c559c8 100644 --- a/admin/src/store/store.js +++ b/admin/src/store/store.js @@ -23,7 +23,7 @@ export const mutations = { export const actions = { logout: ({ commit, state }) => { commit('token', null) - localStorage.clear() + window.localStorage.clear() }, } diff --git a/admin/src/store/store.test.js b/admin/src/store/store.test.js index 81d75f05f..4482a46bf 100644 --- a/admin/src/store/store.test.js +++ b/admin/src/store/store.test.js @@ -1,6 +1,11 @@ -import { mutations } from './store' +import store, { mutations, actions } from './store' const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations +const { logout } = actions + +const CONFIG = { + DEBUG_DISABLE_AUTH: true, +} describe('Vuex store', () => { describe('mutations', () => { @@ -36,4 +41,43 @@ describe('Vuex store', () => { }) }) }) + + describe('actions', () => { + describe('logout', () => { + const windowStorageMock = jest.fn() + const commit = jest.fn() + const state = {} + beforeEach(() => { + jest.clearAllMocks() + window.localStorage.clear = windowStorageMock + }) + + it('deletes the token in store', () => { + logout({ commit, state }) + expect(commit).toBeCalledWith('token', null) + }) + + it.skip('clears the window local storage', () => { + expect(windowStorageMock).toBeCalled() + }) + }) + }) + + describe('state', () => { + describe('authentication enabled', () => { + it('has no token', () => { + expect(store.state.token).toBe(null) + }) + }) + + describe('authentication enabled', () => { + beforeEach(() => { + CONFIG.DEBUG_DISABLE_AUTH = false + }) + + it.skip('has a token', () => { + expect(store.state.token).toBe('validToken') + }) + }) + }) }) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 33dce434b..cdb46c954 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -49,7 +49,7 @@ export class User { @Field(() => number) created: number - @Field(() => Boolean) + @Field(() =>>> Boolean) emailChecked: boolean @Field(() => Boolean) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 64216ca16..6a06fc93c 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -195,24 +195,28 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B @Resolver() export class UserResolver { - /* @Authorized() @Query(() => User) + @UseMiddleware(klicktippNewsletterStateMiddleware) async verifyLogin(@Ctx() context: any): Promise { + // TODO refactor and do not have duplicate code with login(see below) + const userRepository = getCustomRepository(UserRepository) + const userEntity = await userRepository.findByPubkeyHex(context.pubKey) const loginUserRepository = getCustomRepository(LoginUserRepository) - loginUser = loginUserRepository.findByPubkeyHex() - const user = new User(result.data.user) + const loginUser = await loginUserRepository.findByEmail(userEntity.email) + const user = new User() + user.email = userEntity.email + user.firstName = userEntity.firstName + user.lastName = userEntity.lastName + user.username = userEntity.username + user.description = loginUser.description + user.pubkey = userEntity.pubkey.toString('hex') + user.language = loginUser.language - this.email = json.email - this.firstName = json.first_name - this.lastName = json.last_name - this.username = json.username - this.description = json.description - this.pubkey = json.public_hex - this.language = json.language - this.publisherId = json.publisher_id - this.isAdmin = json.isAdmin + // Elopage Status & Stored PublisherId + user.hasElopage = await this.hasElopage(context) + // coinAnimation const userSettingRepository = getCustomRepository(UserSettingRepository) const coinanimation = await userSettingRepository .readBoolean(userEntity.id, Setting.COIN_ANIMATION) @@ -223,7 +227,6 @@ export class UserResolver { user.isAdmin = true // TODO implement return user } - */ @Authorized([RIGHTS.LOGIN]) @Query(() => User) @@ -298,6 +301,7 @@ export class UserResolver { throw new Error(error) }) user.coinanimation = coinanimation + user.isAdmin = true // TODO implement user.isAdmin = true // TODO implement diff --git a/frontend/src/components/SidebarPlugin/SideBar.spec.js b/frontend/src/components/SidebarPlugin/SideBar.spec.js index 8204eb604..7b12b6473 100644 --- a/frontend/src/components/SidebarPlugin/SideBar.spec.js +++ b/frontend/src/components/SidebarPlugin/SideBar.spec.js @@ -3,6 +3,8 @@ import SideBar from './SideBar' const localVue = global.localVue +const storeDispatchMock = jest.fn() + describe('SideBar', () => { let wrapper @@ -23,7 +25,7 @@ describe('SideBar', () => { lastName: 'example', hasElopage: false, }, - commit: jest.fn(), + dispatch: storeDispatchMock, }, $i18n: { locale: 'en', @@ -154,6 +156,42 @@ describe('SideBar', () => { expect(wrapper.emitted('logout')).toEqual([[]]) }) }) + + describe('admin-area', () => { + it('is not visible when not an admin', () => { + expect(wrapper.findAll('li').at(1).text()).not.toBe('admin_area') + }) + + describe('logged in as admin', () => { + const assignLocationSpy = jest.fn() + beforeEach(async () => { + mocks.$store.state.isAdmin = true + mocks.$store.state.token = 'valid-token' + window.location.assign = assignLocationSpy + wrapper = Wrapper() + }) + + it('is visible', () => { + expect(wrapper.findAll('li').at(1).text()).toBe('admin_area') + }) + + describe('click on admin area', () => { + beforeEach(async () => { + await wrapper.findAll('li').at(1).find('a').trigger('click') + }) + + it('opens a new window when clicked', () => { + expect(assignLocationSpy).toHaveBeenCalledWith( + 'http://localhost/admin/authenticate?token=valid-token', + ) + }) + + it('dispatches logout to store', () => { + expect(storeDispatchMock).toHaveBeenCalledWith('logout') + }) + }) + }) + }) }) }) }) diff --git a/frontend/src/components/SidebarPlugin/SideBar.vue b/frontend/src/components/SidebarPlugin/SideBar.vue index 8277b17eb..636cfe2f4 100755 --- a/frontend/src/components/SidebarPlugin/SideBar.vue +++ b/frontend/src/components/SidebarPlugin/SideBar.vue @@ -50,7 +50,15 @@ + + + @@ -122,7 +130,7 @@ export default { this.$emit('logout') }, admin() { - window.location = CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token) + window.location.assign(CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token)) this.$store.dispatch('logout') // logout without redirect }, getElopageLink() { diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 9cd364ed7..8b55f4098 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -20,6 +20,26 @@ export const login = gql` } ` +export const verifyLogin = gql` + query { + verifyLogin { + email + username + firstName + lastName + language + description + coinanimation + klickTipp { + newsletterState + } + hasElopage + publisherId + isAdmin + } + } +` + export const logout = gql` query { logout diff --git a/frontend/src/main.js b/frontend/src/main.js index fd06bf9c0..1aa945608 100755 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -51,7 +51,7 @@ Vue.config.productionTip = false loadAllRules(i18n) -addNavigationGuards(router, store) +addNavigationGuards(router, store, apolloProvider.defaultClient) /* eslint-disable no-new */ new Vue({ diff --git a/frontend/src/routes/guards.js b/frontend/src/routes/guards.js index 005ed720f..0788cb9d8 100644 --- a/frontend/src/routes/guards.js +++ b/frontend/src/routes/guards.js @@ -1,4 +1,6 @@ -const addNavigationGuards = (router, store) => { +import { verifyLogin } from '../graphql/queries' + +const addNavigationGuards = (router, store, apollo) => { // handle publisherId router.beforeEach((to, from, next) => { const publisherId = to.query.pid @@ -10,10 +12,14 @@ const addNavigationGuards = (router, store) => { }) // store token on authenticate - router.beforeEach((to, from, next) => { + router.beforeEach(async (to, from, next) => { if (to.path === '/authenticate' && to.query.token) { - // TODO verify user in order to get user data store.commit('token', to.query.token) + const result = await apollo.query({ + query: verifyLogin, + fetchPolicy: 'network-only', + }) + store.dispatch('login', result.data.verifyLogin) next({ path: '/overview' }) } else { next()