diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 828ec4bd2..2579fa28d 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: 65 + min_coverage: 51 token: ${{ github.token }} ############################################################################## diff --git a/admin/.env.dist b/admin/.env.dist new file mode 100644 index 000000000..6d78e6782 --- /dev/null +++ b/admin/.env.dist @@ -0,0 +1,3 @@ +GRAPHQL_URI=http://localhost:4000/graphql +WALLET_AUTH_URL=http://localhost/vue/authenticate?token=$1 +DEBUG_DISABLE_AUTH=false \ No newline at end of file diff --git a/admin/jest.config.js b/admin/jest.config.js index ac132eed2..b7226bd8f 100644 --- a/admin/jest.config.js +++ b/admin/jest.config.js @@ -1,6 +1,11 @@ module.exports = { verbose: true, - collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'], + collectCoverageFrom: [ + 'src/**/*.{js,vue}', + '!**/node_modules/**', + '!src/assets/**', + '!**/?(*.)+(spec|test).js?(x)', + ], moduleFileExtensions: [ 'js', // 'jsx', diff --git a/admin/package.json b/admin/package.json index 4aebfd131..e3c94f5d8 100644 --- a/admin/package.json +++ b/admin/package.json @@ -32,15 +32,19 @@ "core-js": "^3.6.5", "dotenv-webpack": "^7.0.3", "graphql": "^15.6.1", + "identity-obj-proxy": "^3.0.0", "jest": "26.6.3", + "moment": "^2.29.1", "regenerator-runtime": "^0.13.9", "stats-webpack-plugin": "^0.7.0", "vue": "^2.6.11", "vue-apollo": "^3.0.8", "vue-i18n": "^8.26.5", "vue-jest": "^3.0.7", + "vue-moment": "^4.1.0", "vue-router": "^3.5.3", - "vuex": "^3.6.2" + "vuex": "^3.6.2", + "vuex-persistedstate": "^4.1.0" }, "devDependencies": { "@babel/eslint-parser": "^7.15.8", diff --git a/admin/src/App.spec.js b/admin/src/App.spec.js index b47141972..6936394f1 100644 --- a/admin/src/App.spec.js +++ b/admin/src/App.spec.js @@ -1,43 +1,28 @@ -import { mount } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import App from './App' const localVue = global.localVue -const storeCommitMock = jest.fn() +const stubs = { + RouterView: true, +} const mocks = { $store: { - commit: storeCommitMock, + state: { + token: null, + }, }, } -const localStorageMock = (() => { - let store = {} - - return { - getItem: (key) => { - return store[key] || null - }, - setItem: (key, value) => { - store[key] = value.toString() - }, - removeItem: (key) => { - delete store[key] - }, - clear: () => { - store = {} - }, - } -})() - describe('App', () => { let wrapper const Wrapper = () => { - return mount(App, { localVue, mocks }) + return shallowMount(App, { localVue, stubs, mocks }) } - describe('mount', () => { + describe('shallowMount', () => { beforeEach(() => { wrapper = Wrapper() }) @@ -46,23 +31,4 @@ describe('App', () => { expect(wrapper.find('div#app').exists()).toBeTruthy() }) }) - - describe('window localStorage is undefined', () => { - it('does not commit a token to the store', () => { - expect(storeCommitMock).not.toBeCalled() - }) - }) - - describe('with token in local storage', () => { - beforeEach(() => { - Object.defineProperty(window, 'localStorage', { - value: localStorageMock, - }) - window.localStorage.setItem('vuex', JSON.stringify({ token: 1234 })) - }) - - it.skip('commits the token to the store', () => { - expect(storeCommitMock).toBeCalledWith('token', 1234) - }) - }) }) diff --git a/admin/src/App.vue b/admin/src/App.vue index 9267cc82b..40460eda4 100644 --- a/admin/src/App.vue +++ b/admin/src/App.vue @@ -1,9 +1,15 @@ diff --git a/admin/src/assets/mocks/styleMock.js b/admin/src/assets/mocks/styleMock.js new file mode 100644 index 000000000..4ba52ba2c --- /dev/null +++ b/admin/src/assets/mocks/styleMock.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/admin/src/components/ContentFooter.vue b/admin/src/components/ContentFooter.vue new file mode 100644 index 000000000..ade4e0a83 --- /dev/null +++ b/admin/src/components/ContentFooter.vue @@ -0,0 +1,15 @@ + + diff --git a/admin/src/components/CreationFormular.spec.js b/admin/src/components/CreationFormular.spec.js new file mode 100644 index 000000000..fcdf97cfa --- /dev/null +++ b/admin/src/components/CreationFormular.spec.js @@ -0,0 +1,141 @@ +import { mount } from '@vue/test-utils' +import CreationFormular from './CreationFormular.vue' + +const localVue = global.localVue + +const mocks = { + $moment: jest.fn(() => { + return { + format: jest.fn((m) => m), + subtract: jest.fn(() => { + return { + format: jest.fn((m) => m), + } + }), + } + }), +} + +const propsData = { + type: '', + item: {}, + creation: [], + itemsMassCreation: {}, +} + +describe('CreationFormular', () => { + let wrapper + + const Wrapper = () => { + return mount(CreationFormular, { localVue, mocks, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV element with the class.component-creation-formular', () => { + expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy() + }) + + describe('radio buttons to selcet month', () => { + it('has three radio buttons', () => { + expect(wrapper.findAll('input[type="radio"]').length).toBe(3) + }) + + describe('with mass creation', () => { + beforeEach(async () => { + jest.clearAllMocks() + await wrapper.setProps({ type: 'massCreation' }) + }) + + describe('first radio button', () => { + beforeEach(async () => { + await wrapper.findAll('input[type="radio"]').at(0).setChecked() + }) + + it('emits update-radio-selected with index 0', () => { + expect(wrapper.emitted()['update-radio-selected']).toEqual([ + [expect.arrayContaining([0])], + ]) + }) + }) + + describe('second radio button', () => { + beforeEach(async () => { + await wrapper.findAll('input[type="radio"]').at(1).setChecked() + }) + + it('emits update-radio-selected with index 1', () => { + expect(wrapper.emitted()['update-radio-selected']).toEqual([ + [expect.arrayContaining([1])], + ]) + }) + }) + + describe('third radio button', () => { + beforeEach(async () => { + await wrapper.findAll('input[type="radio"]').at(2).setChecked() + }) + + it('emits update-radio-selected with index 2', () => { + expect(wrapper.emitted()['update-radio-selected']).toEqual([ + [expect.arrayContaining([2])], + ]) + }) + }) + }) + + describe('with single creation', () => { + beforeEach(async () => { + jest.clearAllMocks() + await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] }) + await wrapper.setData({ rangeMin: 180 }) + }) + + describe('first radio button', () => { + beforeEach(async () => { + await wrapper.findAll('input[type="radio"]').at(0).setChecked() + }) + + it('sets rangeMin to 0', () => { + expect(wrapper.vm.rangeMin).toBe(0) + }) + + it('sets rangeMax to 200', () => { + expect(wrapper.vm.rangeMax).toBe(200) + }) + }) + + describe('second radio button', () => { + beforeEach(async () => { + await wrapper.findAll('input[type="radio"]').at(1).setChecked() + }) + + it('sets rangeMin to 0', () => { + expect(wrapper.vm.rangeMin).toBe(0) + }) + + it('sets rangeMax to 400', () => { + expect(wrapper.vm.rangeMax).toBe(400) + }) + }) + + describe('third radio button', () => { + beforeEach(async () => { + await wrapper.findAll('input[type="radio"]').at(2).setChecked() + }) + + it('sets rangeMin to 0', () => { + expect(wrapper.vm.rangeMin).toBe(0) + }) + + it('sets rangeMax to 400', () => { + expect(wrapper.vm.rangeMax).toBe(600) + }) + }) + }) + }) + }) +}) diff --git a/admin/src/components/CreationFormular.vue b/admin/src/components/CreationFormular.vue new file mode 100644 index 000000000..d6b637152 --- /dev/null +++ b/admin/src/components/CreationFormular.vue @@ -0,0 +1,290 @@ + + diff --git a/admin/src/components/NavBar.spec.js b/admin/src/components/NavBar.spec.js new file mode 100644 index 000000000..1d68b16ad --- /dev/null +++ b/admin/src/components/NavBar.spec.js @@ -0,0 +1,30 @@ +import { mount } from '@vue/test-utils' +import NavBar from './NavBar.vue' + +const localVue = global.localVue + +const mocks = { + $store: { + state: { + openCreations: 1, + }, + }, +} + +describe('NavBar', () => { + let wrapper + + const Wrapper = () => { + return mount(NavBar, { mocks, localVue }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV element with the class.component-nabvar', () => { + expect(wrapper.find('.component-nabvar').exists()).toBeTruthy() + }) + }) +}) diff --git a/admin/src/components/NavBar.vue b/admin/src/components/NavBar.vue new file mode 100644 index 000000000..c52743857 --- /dev/null +++ b/admin/src/components/NavBar.vue @@ -0,0 +1,58 @@ + + diff --git a/admin/src/components/UserTable.spec.js b/admin/src/components/UserTable.spec.js new file mode 100644 index 000000000..3db0131a3 --- /dev/null +++ b/admin/src/components/UserTable.spec.js @@ -0,0 +1,29 @@ +import { mount } from '@vue/test-utils' +import UserTable from './UserTable.vue' + +const localVue = global.localVue + +describe('UserTable', () => { + let wrapper + + const propsData = { + type: 'Type', + itemsUser: [], + fieldsTable: [], + creation: [], + } + + const Wrapper = () => { + return mount(UserTable, { localVue, propsData }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV element with the class.component-user-table', () => { + expect(wrapper.find('.component-user-table').exists()).toBeTruthy() + }) + }) +}) diff --git a/admin/src/components/UserTable.vue b/admin/src/components/UserTable.vue new file mode 100644 index 000000000..265c2d12e --- /dev/null +++ b/admin/src/components/UserTable.vue @@ -0,0 +1,254 @@ + + + + diff --git a/admin/src/config/index.js b/admin/src/config/index.js index eab63e903..69d30a66a 100644 --- a/admin/src/config/index.js +++ b/admin/src/config/index.js @@ -17,8 +17,13 @@ const environment = { PRODUCTION: process.env.NODE_ENV === 'production' || false, } -const server = { +const endpoints = { GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql', + WALLET_AUTH_URL: process.env.WALLET_AUTH_URL || 'http://localhost/vue/authenticate?token=$1', +} + +const debug = { + DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' || false, } const options = {} @@ -26,8 +31,9 @@ const options = {} const CONFIG = { ...version, ...environment, - ...server, + ...endpoints, ...options, + ...debug, } export default CONFIG diff --git a/admin/src/graphql/searchUsers.js b/admin/src/graphql/searchUsers.js new file mode 100644 index 000000000..86e333845 --- /dev/null +++ b/admin/src/graphql/searchUsers.js @@ -0,0 +1,12 @@ +import gql from 'graphql-tag' + +export const searchUsers = gql` + query ($searchText: String!) { + searchUsers(searchText: $searchText) { + firstName + lastName + email + creation + } + } +` diff --git a/admin/src/i18n.test.js b/admin/src/i18n.test.js new file mode 100644 index 000000000..e39e0e824 --- /dev/null +++ b/admin/src/i18n.test.js @@ -0,0 +1,30 @@ +import i18n from './i18n' +import VueI18n from 'vue-i18n' + +jest.mock('vue-i18n') + +describe('i18n', () => { + it('calls i18n with locale en', () => { + expect(VueI18n).toBeCalledWith( + expect.objectContaining({ + locale: 'en', + }), + ) + }) + + it('calls i18n with fallback locale en', () => { + expect(VueI18n).toBeCalledWith( + expect.objectContaining({ + fallbackLocale: 'en', + }), + ) + }) + + it('has a _t function', () => { + expect(i18n).toEqual( + expect.objectContaining({ + _t: expect.anything(), + }), + ) + }) +}) diff --git a/admin/src/layouts/defaultLayout.vue b/admin/src/layouts/defaultLayout.vue new file mode 100644 index 000000000..28babdd58 --- /dev/null +++ b/admin/src/layouts/defaultLayout.vue @@ -0,0 +1,19 @@ + + + diff --git a/admin/src/main.js b/admin/src/main.js index 7375393d9..3be3ae0bf 100644 --- a/admin/src/main.js +++ b/admin/src/main.js @@ -16,12 +16,17 @@ import VueApollo from 'vue-apollo' import CONFIG from './config' -import { BootstrapVue } from 'bootstrap-vue' +import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-vue/dist/bootstrap-vue.css' + +import moment from 'vue-moment' const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI }) const authLink = new ApolloLink((operation, forward) => { const token = store.state.token + operation.setContext({ headers: { Authorization: token && token.length > 0 ? `Bearer ${token}` : '', @@ -52,9 +57,16 @@ const apolloProvider = new VueApollo({ Vue.use(BootstrapVue) +Vue.use(IconsPlugin) + +Vue.use(moment) + +Vue.use(VueApollo) + addNavigationGuards(router, store) new Vue({ + moment, router, store, i18n, diff --git a/admin/src/main.test.js b/admin/src/main.test.js index 27c8898ab..c639593d6 100644 --- a/admin/src/main.test.js +++ b/admin/src/main.test.js @@ -5,10 +5,13 @@ import CONFIG from './config' import Vue from 'vue' import Vuex from 'vuex' import VueI18n from 'vue-i18n' +import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' +import moment from 'vue-moment' jest.mock('vue') jest.mock('vuex') jest.mock('vue-i18n') +jest.mock('vue-moment') const storeMock = jest.fn() Vuex.Store = storeMock @@ -25,6 +28,16 @@ jest.mock('apollo-boost', () => { } }) +jest.mock('bootstrap-vue', () => { + return { + __esModule: true, + BootstrapVue: jest.fn(), + IconsPlugin: jest.fn(() => { + return { concat: jest.fn() } + }), + } +}) + describe('main', () => { it('calls the HttpLink', () => { expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI }) @@ -50,6 +63,18 @@ describe('main', () => { expect(VueI18n).toBeCalled() }) + it.skip('calls BootstrapVue', () => { + expect(BootstrapVue).toBeCalled() + }) + + it.skip('calls IconsPlugin', () => { + expect(IconsPlugin).toBeCalled() + }) + + it.skip('calls Moment', () => { + expect(moment).toBeCalled() + }) + it.skip('creates a store', () => { expect(storeMock).toBeCalled() }) diff --git a/admin/src/pages/Creation.spec.js b/admin/src/pages/Creation.spec.js new file mode 100644 index 000000000..02c2ed4ce --- /dev/null +++ b/admin/src/pages/Creation.spec.js @@ -0,0 +1,59 @@ +import { mount } from '@vue/test-utils' +import Creation from './Creation.vue' + +const localVue = global.localVue + +const apolloQueryMock = jest.fn().mockResolvedValue({ + data: { + searchUsers: [ + { + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + creation: [200, 400, 600], + }, + ], + }, +}) + +const toastErrorMock = jest.fn() + +const mocks = { + $apollo: { + query: apolloQueryMock, + }, + $toasted: { + error: toastErrorMock, + }, +} + +describe('Creation', () => { + let wrapper + + const Wrapper = () => { + return mount(Creation, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV element with the class.creation', () => { + expect(wrapper.find('div.creation').exists()).toBeTruthy() + }) + + describe('apollo returns error', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ + message: 'Ouch', + }) + wrapper = Wrapper() + }) + + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Ouch') + }) + }) + }) +}) diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue new file mode 100644 index 000000000..7ab900b43 --- /dev/null +++ b/admin/src/pages/Creation.vue @@ -0,0 +1,143 @@ + + diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js new file mode 100644 index 000000000..caf94cd37 --- /dev/null +++ b/admin/src/pages/CreationConfirm.spec.js @@ -0,0 +1,53 @@ +import { mount } from '@vue/test-utils' +import CreationConfirm from './CreationConfirm.vue' + +const localVue = global.localVue + +const storeCommitMock = jest.fn() + +const mocks = { + $store: { + commit: storeCommitMock, + }, +} + +describe('CreationConfirm', () => { + let wrapper + + const Wrapper = () => { + return mount(CreationConfirm, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper = Wrapper() + }) + + it('has a DIV element with the class.creation-confirm', () => { + expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy() + }) + + describe('store', () => { + it('commits resetOpenCreations to store', () => { + expect(storeCommitMock).toBeCalledWith('resetOpenCreations') + }) + + it('commits openCreationsPlus to store', () => { + expect(storeCommitMock).toBeCalledWith('openCreationsPlus', 5) + }) + }) + + describe('confirm creation', () => { + beforeEach(async () => { + await wrapper + .findComponent({ name: 'UserTable' }) + .vm.$emit('remove-confirm-result', 1, 'remove') + }) + + it('commits openCreationsMinus to store', () => { + expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1) + }) + }) + }) +}) diff --git a/admin/src/pages/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue new file mode 100644 index 000000000..0d68635e0 --- /dev/null +++ b/admin/src/pages/CreationConfirm.vue @@ -0,0 +1,149 @@ + + diff --git a/admin/src/pages/Overview.vue b/admin/src/pages/Overview.vue new file mode 100644 index 000000000..056bb14a6 --- /dev/null +++ b/admin/src/pages/Overview.vue @@ -0,0 +1,82 @@ + + diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js new file mode 100644 index 000000000..37ba4f5ec --- /dev/null +++ b/admin/src/pages/UserSearch.spec.js @@ -0,0 +1,59 @@ +import { mount } from '@vue/test-utils' +import UserSearch from './UserSearch.vue' + +const localVue = global.localVue + +const apolloQueryMock = jest.fn().mockResolvedValue({ + data: { + searchUsers: [ + { + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + creation: [200, 400, 600], + }, + ], + }, +}) + +const toastErrorMock = jest.fn() + +const mocks = { + $apollo: { + query: apolloQueryMock, + }, + $toasted: { + error: toastErrorMock, + }, +} + +describe('UserSearch', () => { + let wrapper + + const Wrapper = () => { + return mount(UserSearch, { localVue, mocks }) + } + + describe('mount', () => { + beforeEach(() => { + wrapper = Wrapper() + }) + + it('has a DIV element with the class.user-search', () => { + expect(wrapper.find('div.user-search').exists()).toBeTruthy() + }) + + describe('apollo returns error', () => { + beforeEach(() => { + apolloQueryMock.mockRejectedValue({ + message: 'Ouch', + }) + wrapper = Wrapper() + }) + + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Ouch') + }) + }) + }) +}) diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue new file mode 100644 index 000000000..ae0ade7b2 --- /dev/null +++ b/admin/src/pages/UserSearch.vue @@ -0,0 +1,70 @@ + + diff --git a/admin/src/router/guards.js b/admin/src/router/guards.js index c9baf61cb..d59234a25 100644 --- a/admin/src/router/guards.js +++ b/admin/src/router/guards.js @@ -1,7 +1,25 @@ +import CONFIG from '../config' + const addNavigationGuards = (router, store) => { + // store token on `authenticate` router.beforeEach((to, from, next) => { - // handle authentication - if (to.meta.requiresAuth && !store.state.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: '/' }) + } else { + next() + } + }) + + // protect all routes but `not-found` + 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 + to.path !== '/not-found' && // we are not on `not-found` + to.path !== '/logout' // we are not on `logout` + ) { next({ path: '/not-found' }) } else { next() 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 new file mode 100644 index 000000000..eb9b646cb --- /dev/null +++ b/admin/src/router/router.test.js @@ -0,0 +1,92 @@ +import router from './router' + +describe('router', () => { + describe('options', () => { + const { options } = router + const { scrollBehavior, routes } = options + + it('has "/admin" as base', () => { + expect(options).toEqual( + expect.objectContaining({ + base: '/admin', + }), + ) + }) + + it('has "active" as linkActiveClass', () => { + expect(options).toEqual( + expect.objectContaining({ + linkActiveClass: 'active', + }), + ) + }) + + it('has "history" as mode', () => { + expect(options).toEqual( + expect.objectContaining({ + mode: 'history', + }), + ) + }) + + describe('scroll behavior', () => { + it('returns save position when given', () => { + expect(scrollBehavior({}, {}, 'given')).toBe('given') + }) + + it('returns selector when hash is given', () => { + expect(scrollBehavior({ hash: '#to' }, {})).toEqual({ selector: '#to' }) + }) + + it('returns top left coordinates as default', () => { + expect(scrollBehavior({}, {})).toEqual({ x: 0, y: 0 }) + }) + }) + + 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') + }) + + 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') + }) + }) + + describe('user', () => { + it('loads the "UserSearch" component', async () => { + const component = await routes.find((r) => r.path === '/user').component() + expect(component.default.name).toBe('UserSearch') + }) + }) + + describe('creation', () => { + it('loads the "Creation" component', async () => { + const component = await routes.find((r) => r.path === '/creation').component() + expect(component.default.name).toBe('Creation') + }) + }) + + describe('creation-confirm', () => { + it('loads the "CreationConfirm" component', async () => { + const component = await routes.find((r) => r.path === '/creation-confirm').component() + expect(component.default.name).toBe('CreationConfirm') + }) + }) + + describe('not found page', () => { + it('renders the "NotFound" component', async () => { + const component = await routes.find((r) => r.path === '*').component() + expect(component.default.name).toEqual('not-found') + }) + }) + }) + }) +}) diff --git a/admin/src/router/routes.js b/admin/src/router/routes.js index 604b03bee..72e7b1ac5 100644 --- a/admin/src/router/routes.js +++ b/admin/src/router/routes.js @@ -1,15 +1,32 @@ -import NotFound from '@/components/NotFoundPage.vue' - const routes = [ { - path: '/', - /* - meta: { - requiresAuth: true, - }, - */ + path: '/authenticate', + }, + { + path: '/', + component: () => import('@/pages/Overview.vue'), + }, + { + // TODO: Implement a "You are logged out"-Page + path: '/logout', + component: () => import('@/components/NotFoundPage.vue'), + }, + { + path: '/user', + component: () => import('@/pages/UserSearch.vue'), + }, + { + path: '/creation', + component: () => import('@/pages/Creation.vue'), + }, + { + path: '/creation-confirm', + component: () => import('@/pages/CreationConfirm.vue'), + }, + { + path: '*', + component: () => import('@/components/NotFoundPage.vue'), }, - { path: '*', component: NotFound }, ] export default routes diff --git a/admin/src/store/store.js b/admin/src/store/store.js index 709ac52d0..754c559c8 100644 --- a/admin/src/store/store.js +++ b/admin/src/store/store.js @@ -1,19 +1,46 @@ import Vuex from 'vuex' import Vue from 'vue' +import createPersistedState from 'vuex-persistedstate' +import CONFIG from '../config' Vue.use(Vuex) export const mutations = { + openCreationsPlus: (state, i) => { + state.openCreations += i + }, + openCreationsMinus: (state, i) => { + state.openCreations -= i + }, + resetOpenCreations: (state) => { + state.openCreations = 0 + }, token: (state, token) => { state.token = token }, } -const store = new Vuex.Store({ - mutations, - state: { - token: 'some-token', +export const actions = { + logout: ({ commit, state }) => { + commit('token', null) + window.localStorage.clear() }, +} + +const store = new Vuex.Store({ + plugins: [ + createPersistedState({ + storage: window.localStorage, + }), + ], + state: { + token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null, + moderator: 'Dertest Moderator', + openCreations: 0, + }, + // Syncronous mutation of the state + mutations, + actions, }) export default store diff --git a/admin/src/store/store.test.js b/admin/src/store/store.test.js index 9ab9d980b..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 } = mutations +const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations +const { logout } = actions + +const CONFIG = { + DEBUG_DISABLE_AUTH: true, +} describe('Vuex store', () => { describe('mutations', () => { @@ -11,5 +16,68 @@ describe('Vuex store', () => { expect(state.token).toEqual('1234') }) }) + + describe('openCreationsPlus', () => { + it('increases the open creations by a given number', () => { + const state = { openCreations: 0 } + openCreationsPlus(state, 12) + expect(state.openCreations).toEqual(12) + }) + }) + + describe('openCreationsMinus', () => { + it('decreases the open creations by a given number', () => { + const state = { openCreations: 12 } + openCreationsMinus(state, 2) + expect(state.openCreations).toEqual(10) + }) + }) + + describe('resetOpenCreations', () => { + it('sets the open creations to 0', () => { + const state = { openCreations: 24 } + resetOpenCreations(state) + expect(state.openCreations).toEqual(0) + }) + }) + }) + + 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/admin/test/testSetup.js b/admin/test/testSetup.js index 3b6b50218..caaa3c19c 100644 --- a/admin/test/testSetup.js +++ b/admin/test/testSetup.js @@ -1,6 +1,6 @@ import { createLocalVue } from '@vue/test-utils' import Vue from 'vue' -import { BootstrapVue } from 'bootstrap-vue' +import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' // without this async calls are not working import 'regenerator-runtime' @@ -8,6 +8,7 @@ import 'regenerator-runtime' global.localVue = createLocalVue() global.localVue.use(BootstrapVue) +global.localVue.use(IconsPlugin) // throw errors for vue warnings to force the programmers to take care about warnings Vue.config.warnHandler = (w) => { diff --git a/admin/yarn.lock b/admin/yarn.lock index fbc12c105..d7960320b 100644 --- a/admin/yarn.lock +++ b/admin/yarn.lock @@ -6424,6 +6424,11 @@ har-validator@~5.1.3: ajv "^6.12.3" har-schema "^2.0.0" +harmony-reflect@^1.4.6: + version "1.6.2" + resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710" + integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -6776,6 +6781,13 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" +identity-obj-proxy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" + integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + dependencies: + harmony-reflect "^1.4.6" + ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -9020,6 +9032,11 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1: dependencies: minimist "^1.2.5" +moment@^2.19.2, moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -11163,6 +11180,11 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +shvl@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1" + integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw== + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -12469,6 +12491,13 @@ vue-loader@^15.9.2: vue-hot-reload-api "^2.3.0" vue-style-loader "^4.1.0" +vue-moment@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vue-moment/-/vue-moment-4.1.0.tgz#092a8ff723a96c6f85a0a8e23ad30f0bf320f3b0" + integrity sha512-Gzisqpg82ItlrUyiD9d0Kfru+JorW2o4mQOH06lEDZNgxci0tv/fua1Hl0bo4DozDV2JK1r52Atn/8QVCu8qQw== + dependencies: + moment "^2.19.2" + vue-router@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.3.tgz#041048053e336829d05dafacf6a8fb669a2e7999" @@ -12500,6 +12529,14 @@ vue@^2.6.11: resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235" integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ== +vuex-persistedstate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53" + integrity sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ== + dependencies: + deepmerge "^4.2.2" + shvl "^2.0.3" + vuex@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71" diff --git a/backend/.env.dist b/backend/.env.dist index 9b974d8f9..ed9b26102 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -28,4 +28,6 @@ COMMUNITY_URL= COMMUNITY_REGISTER_URL= COMMUNITY_DESCRIPTION= LOGIN_APP_SECRET=21ffbbc616fe -LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a \ No newline at end of file +LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a + +WEBHOOK_ELOPAGE_SECRET=secret \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index bc098958f..375046363 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,6 +20,7 @@ "apollo-server-express": "^2.25.2", "apollo-server-testing": "^2.25.2", "axios": "^0.21.1", + "body-parser": "^1.19.0", "class-validator": "^0.13.1", "cors": "^2.8.5", "dotenv": "^10.0.0", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index ed11641cd..a12524a78 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -54,9 +54,21 @@ const email = { EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1', } +const webhook = { + WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', +} + // This is needed by graphql-directive-auth process.env.APP_SECRET = server.JWT_SECRET -const CONFIG = { ...server, ...database, ...klicktipp, ...community, ...email, ...loginServer } +const CONFIG = { + ...server, + ...database, + ...klicktipp, + ...community, + ...email, + ...loginServer, + ...webhook, +} export default CONFIG diff --git a/backend/src/graphql/arg/CreateUserArgs.ts b/backend/src/graphql/arg/CreateUserArgs.ts index 75897e3fc..0d63e76bb 100644 --- a/backend/src/graphql/arg/CreateUserArgs.ts +++ b/backend/src/graphql/arg/CreateUserArgs.ts @@ -12,7 +12,7 @@ export default class CreateUserArgs { lastName: string @Field(() => String) - language: string + language?: string // Will default to DEFAULT_LANGUAGE @Field(() => Int, { nullable: true }) publisherId: number diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 5b7682e01..cdb46c954 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -20,6 +20,7 @@ export class User { this.pubkey = json.public_hex this.language = json.language this.publisherId = json.publisher_id + this.isAdmin = json.isAdmin } } @@ -48,7 +49,7 @@ export class User { @Field(() => number) created: number - @Field(() => Boolean) + @Field(() =>>> Boolean) emailChecked: boolean @Field(() => Boolean) @@ -71,6 +72,9 @@ export class User { @Field(() => Int, { nullable: true }) publisherId?: number + @Field(() => Boolean) + isAdmin: boolean + @Field(() => Boolean) coinanimation: boolean diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts new file mode 100644 index 000000000..cbf8dd21c --- /dev/null +++ b/backend/src/graphql/model/UserAdmin.ts @@ -0,0 +1,16 @@ +import { ObjectType, Field } from 'type-graphql' + +@ObjectType() +export class UserAdmin { + @Field(() => String) + email: string + + @Field(() => String) + firstName: string + + @Field(() => String) + lastName: string + + @Field(() => [Number]) + creation: number[] +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts new file mode 100644 index 000000000..9af50faad --- /dev/null +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -0,0 +1,26 @@ +import { Resolver, Query, Arg } from 'type-graphql' +import { getCustomRepository } from 'typeorm' +import { UserAdmin } from '../model/UserAdmin' +import { LoginUserRepository } from '../../typeorm/repository/LoginUser' + +@Resolver() +export class AdminResolver { + @Query(() => [UserAdmin]) + async searchUsers(@Arg('searchText') searchText: string): Promise { + const loginUserRepository = getCustomRepository(LoginUserRepository) + const loginUsers = await loginUserRepository.findBySearchCriteria(searchText) + const users = loginUsers.map((loginUser) => { + const user = new UserAdmin() + user.firstName = loginUser.firstName + user.lastName = loginUser.lastName + user.email = loginUser.email + user.creation = [ + (Math.floor(Math.random() * 50) + 1) * 20, + (Math.floor(Math.random() * 50) + 1) * 20, + (Math.floor(Math.random() * 50) + 1) * 20, + ] + return user + }) + return users + } +} diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index b7c077250..840538bd4 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -148,6 +148,69 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B @Resolver() export class UserResolver { + /* + @Authorized() + @Query(() => User) + async verifyLogin(@Ctx() context: any): Promise { + const loginUserRepository = getCustomRepository(LoginUserRepository) + loginUser = loginUserRepository.findByPubkeyHex() + const user = new User(result.data.user) + + 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 + + const userSettingRepository = getCustomRepository(UserSettingRepository) + const coinanimation = await userSettingRepository + .readBoolean(userEntity.id, Setting.COIN_ANIMATION) + .catch((error) => { + throw new Error(error) + }) + user.coinanimation = coinanimation + user.isAdmin = true // TODO implement + return user + } + */ + + @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) + 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 + + // 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) + .catch((error) => { + throw new Error(error) + }) + user.coinanimation = coinanimation + user.isAdmin = true // TODO implement + return user + } + @Query(() => User) @UseMiddleware(klicktippNewsletterStateMiddleware) async login( @@ -229,6 +292,7 @@ export class UserResolver { throw new Error(error) }) user.coinanimation = coinanimation + user.isAdmin = true // TODO implement context.setHeaders.push({ key: 'token', @@ -257,7 +321,7 @@ export class UserResolver { // default int publisher_id = 0; // Validate Language (no throw) - if (!isLanguage(language)) { + if (!language || !isLanguage(language)) { language = DEFAULT_LANGUAGE } diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 4350483ff..28e0e1ce4 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -6,6 +6,7 @@ import 'module-alias/register' import { ApolloServer } from 'apollo-server-express' import express from 'express' +import bodyParser from 'body-parser' // database import connection from '../typeorm/connection' @@ -22,6 +23,9 @@ import CONFIG from '../config' // graphql import schema from '../graphql/schema' +// webhooks +import { elopageWebhook } from '../webhook/elopage' + // TODO implement // import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity"; @@ -50,6 +54,12 @@ const createServer = async (context: any = serverContext): Promise => { // cors app.use(cors) + // bodyparser + app.use(bodyParser.json()) + + // Elopage Webhook + app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook) + // Apollo Server const apollo = new ApolloServer({ schema: await schema(), diff --git a/backend/src/typeorm/repository/LoginUser.ts b/backend/src/typeorm/repository/LoginUser.ts index 65ac6f67b..ac7ff31b6 100644 --- a/backend/src/typeorm/repository/LoginUser.ts +++ b/backend/src/typeorm/repository/LoginUser.ts @@ -8,4 +8,17 @@ export class LoginUserRepository extends Repository { .where('loginUser.email = :email', { email }) .getOneOrFail() } + + async findBySearchCriteria(searchCriteria: string): Promise { + return await this.createQueryBuilder('user') + .where( + 'user.firstName like :name or user.lastName like :lastName or user.email like :email', + { + name: `%${searchCriteria}%`, + lastName: `%${searchCriteria}%`, + email: `%${searchCriteria}%`, + }, + ) + .getMany() + } } diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts new file mode 100644 index 000000000..90cdb159f --- /dev/null +++ b/backend/src/webhook/elopage.ts @@ -0,0 +1,154 @@ +/* + Elopage Webhook + + Those are all available infos: + HandleElopageRequestTask: order_id 849951 + + Es gibt 5 elopage request mit dieser order_id + Alle von der gleichen Person, aber unterschiedliche Events: + 2019-12-03: chargeback.successful + 29.10.2019: order.subscription.paused + 2019-12-06: payment.successful + 29.10.2019: order.subscription.paying + 2091-12-07: payment.pending + + + order_id=849951&order_token=y22MJxHr9XzzPiaaH9GU&payment_session_id=849951&payment_session_token=y22MJxHr9XzzPiaaH9GU&action=payment_processed&initiator&payer[email]=theodora.mis%40gmx.ch&payer[first_name]=Theodora&payer[last_name]=Mis&payer[country]=Schweiz&payer[country_code]=CH&payer[city]=St.+Gallen&payer[street]=Vonwilstrasse+23&payer[street_number]&payer[zip]=9000&payer[company]&payer[vat_no]&payer[phone]&gift_receiver&publisher[id]=691&publisher[email]=joytopia%40gmail.com&publisher[first_name]=Bernd&publisher[last_name]=H%C3%BCckst%C3%A4dt&publisher[street]=Pfarrweg+2&publisher[zip]=74653&publisher[city]=K%C3%BCnzelsau&publisher[country]=Deutschland&publisher[phone]=%2B4979405460810&team_members&product_id=43944&product[id]=43944&product[slug]=gold-de&product[name]=Gold-Mitgliedschaft&product[type]=membership&product[price]=40.0&product[affiliate_program_id]=111&upsell&membership[id]=43944&membership[name]=Gold-Mitgliedschaft&membership[membership_product_1]=Werkzeuge+%26+Ressourcen+%28Gold%29&membership[membership_product_1_id]=44982&membership[membership_product_2]=Zertifizierung+zum%2Fr+Gradido-Botschafter%2Fin&membership[membership_product_2_id]=43970&membership[membership_product_3]=Seminar+3+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_3_id]=43969&membership[membership_product_4]=Potential-Entfaltungs-Techniken&membership[membership_product_4_id]=43954&membership[membership_product_5]=Seminar+2+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_5_id]=43896&membership[membership_product_6]=Kongresspaket%3A+Gesundes+Geld+f%C3%BCr+eine+gesunde+Welt&membership[membership_product_6_id]=14590&membership[membership_product_7]=Deine+Gold-Mitgliedschaft+bei+Gradido&membership[membership_product_7_id]=43951&membership[membership_product_8]=Gradido+E-Book%2C+H%C3%B6rspiel+%22Joytopia%22++%E2%80%93+und+100+Vorteile&membership[membership_product_8_id]=7312&membership[membership_product_9]=Danke%2C+dass+Du+hilfst+Gradido+in+die+Welt+zu+bringen%21&membership[membership_product_9_id]=43744&membership[membership_product_10]=Basis-Informationen+zu+Gradido&membership[membership_product_10_id]=42600&membership[membership_product_11]=Seminar+1+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_11_id]=43882&membership[membership_product_12]=Musical+%22Gradido+%E2%80%93+gemeinsam+retten+wir+die+Welt%22&membership[membership_product_12_id]=43886&membership[membership_product_13]=Premium+Community+%26+Markt&membership[membership_product_13_id]=43885&membership[membership_product_14]=Gradido+Buch+ungek%C3%BCrzte+Version+%26+%C3%9Cbersetzungen&membership[membership_product_14_id]=43887&membership[membership_product_15]=Online-Konferenzen&membership[membership_product_15_id]=43919&membership[membership_product_16]=Gradido+H%C3%B6rbuch&membership[membership_product_16_id]=43920&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&pricing_plan[name]=Monatlich&add_id_1&add_id_2&campaign_id¤cy=EUR&coupon_code&recurring=yes&recurring_form=subscription&payment_state=payment_paused&payment_method=sepa&opt_ins&payments_schedule[][rate]=1&payments_schedule[][state]=debt&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2019&payments_schedule[][rate]=2&payments_schedule[][state]=pending&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.11.2019&payments_schedule[][rate]=3&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.12.2019&payments_schedule[][rate]=4&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.01.2020&payments_schedule[][rate]=5&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.02.2020&payments_schedule[][rate]=6&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.03.2020&payments_schedule[][rate]=7&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.04.2020&payments_schedule[][rate]=8&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.05.2020&payments_schedule[][rate]=9&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.06.2020&payments_schedule[][rate]=10&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.07.2020&payments_schedule[][rate]=11&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.08.2020&payments_schedule[][rate]=12&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.09.2020&payments_schedule[][rate]=13&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2020&payments_schedule[][rate]=14&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.11.2020&payments_count=0&payments_count_expected&with_test_period=false&with_custom_start=false&created=29.10.2019+13%3A17&id=58268076&invoice_number&revenue=-40.0&amount=-23.72&fee=-16.28&vat_rate=0.0&vat_amount=0.0&state=successful&refunded_transfer_id=52876337&invoice_link&credit_memo_link=http%3A%2F%2Felopage.com%2Fcommon%2Fcredit_memos%2F12410%3Ftoken%3D6dyBsddt6gsJpX8Fq-M2&success_link=http%3A%2F%2Felopage.com%2Fs%2Fgradido%2Fpayment%2Fy22MJxHr9XzzPiaaH9GU&error_msg&created_date=2019-12-03T22%3A15Z&success_date=2019-12-03T22%3A15Z&success_date_short=2019-12-03&created_date_utc=03.12.2019+22%3A15&success_date_utc=03.12.2019+22%3A15&team_member_commissions&event=chargeback.successful + order_id=849951&order_token=y22MJxHr9XzzPiaaH9GU&payment_session_id=849951&payment_session_token=y22MJxHr9XzzPiaaH9GU&action=subscription_state_changed&initiator&payer[email]=theodora.mis%40gmx.ch&payer[first_name]=Theodora&payer[last_name]=Mis&payer[country]=Schweiz&payer[country_code]=CH&payer[city]=St.+Gallen&payer[street]=Vonwilstrasse+23&payer[street_number]&payer[zip]=9000&payer[company]&payer[vat_no]&payer[phone]&gift_receiver&publisher[id]=691&publisher[email]=joytopia%40gmail.com&publisher[first_name]=Bernd&publisher[last_name]=H%C3%BCckst%C3%A4dt&publisher[street]=Pfarrweg+2&publisher[zip]=74653&publisher[city]=K%C3%BCnzelsau&publisher[country]=Deutschland&publisher[phone]=%2B4979405460810&team_members&product_id=43944&product[id]=43944&product[slug]=gold-de&product[name]=Gold-Mitgliedschaft&product[type]=membership&product[price]=40.0&product[affiliate_program_id]=111&upsell&membership[id]=43944&membership[name]=Gold-Mitgliedschaft&membership[membership_product_1]=Werkzeuge+%26+Ressourcen+%28Gold%29&membership[membership_product_1_id]=44982&membership[membership_product_2]=Zertifizierung+zum%2Fr+Gradido-Botschafter%2Fin&membership[membership_product_2_id]=43970&membership[membership_product_3]=Seminar+3+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_3_id]=43969&membership[membership_product_4]=Potential-Entfaltungs-Techniken&membership[membership_product_4_id]=43954&membership[membership_product_5]=Seminar+2+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_5_id]=43896&membership[membership_product_6]=Kongresspaket%3A+Gesundes+Geld+f%C3%BCr+eine+gesunde+Welt&membership[membership_product_6_id]=14590&membership[membership_product_7]=Deine+Gold-Mitgliedschaft+bei+Gradido&membership[membership_product_7_id]=43951&membership[membership_product_8]=Gradido+E-Book%2C+H%C3%B6rspiel+%22Joytopia%22++%E2%80%93+und+100+Vorteile&membership[membership_product_8_id]=7312&membership[membership_product_9]=Danke%2C+dass+Du+hilfst+Gradido+in+die+Welt+zu+bringen%21&membership[membership_product_9_id]=43744&membership[membership_product_10]=Basis-Informationen+zu+Gradido&membership[membership_product_10_id]=42600&membership[membership_product_11]=Seminar+1+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_11_id]=43882&membership[membership_product_12]=Musical+%22Gradido+%E2%80%93+gemeinsam+retten+wir+die+Welt%22&membership[membership_product_12_id]=43886&membership[membership_product_13]=Premium+Community+%26+Markt&membership[membership_product_13_id]=43885&membership[membership_product_14]=Gradido+Buch+ungek%C3%BCrzte+Version+%26+%C3%9Cbersetzungen&membership[membership_product_14_id]=43887&membership[membership_product_15]=Online-Konferenzen&membership[membership_product_15_id]=43919&membership[membership_product_16]=Gradido+H%C3%B6rbuch&membership[membership_product_16_id]=43920&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&pricing_plan[name]=Monatlich&add_id_1&add_id_2&campaign_id¤cy=EUR&coupon_code&recurring=yes&recurring_form=subscription&payment_state=payment_paused&payment_method=sepa&opt_ins&payments_schedule[][rate]=1&payments_schedule[][state]=debt&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2019&payments_schedule[][rate]=2&payments_schedule[][state]=pending&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.11.2019&payments_schedule[][rate]=3&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.12.2019&payments_schedule[][rate]=4&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.01.2020&payments_schedule[][rate]=5&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.02.2020&payments_schedule[][rate]=6&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.03.2020&payments_schedule[][rate]=7&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.04.2020&payments_schedule[][rate]=8&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.05.2020&payments_schedule[][rate]=9&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.06.2020&payments_schedule[][rate]=10&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.07.2020&payments_schedule[][rate]=11&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.08.2020&payments_schedule[][rate]=12&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.09.2020&payments_schedule[][rate]=13&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2020&payments_schedule[][rate]=14&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.11.2020&payments_count=0&payments_count_expected&with_test_period=false&with_custom_start=false&created=29.10.2019+13%3A17&event=order.subscription.paused + order_id=849951&order_token=y22MJxHr9XzzPiaaH9GU&payment_session_id=849951&payment_session_token=y22MJxHr9XzzPiaaH9GU&action=payment_processed&initiator&payer[email]=theodora.mis%40gmx.ch&payer[first_name]=Theodora&payer[last_name]=Mis&payer[country]=Schweiz&payer[country_code]=CH&payer[city]=St.+Gallen&payer[street]=Vonwilstrasse+23&payer[street_number]&payer[zip]=9000&payer[company]&payer[vat_no]&payer[phone]&gift_receiver&publisher[id]=691&publisher[email]=joytopia%40gmail.com&publisher[first_name]=Bernd&publisher[last_name]=H%C3%BCckst%C3%A4dt&publisher[street]=Pfarrweg+2&publisher[zip]=74653&publisher[city]=K%C3%BCnzelsau&publisher[country]=Deutschland&publisher[phone]=%2B4979405460810&team_members&product_id=43944&product[id]=43944&product[slug]=gold-de&product[name]=Gold-Mitgliedschaft&product[type]=membership&product[price]=40.0&product[affiliate_program_id]=111&upsell&membership[id]=43944&membership[name]=Gold-Mitgliedschaft&membership[membership_product_1]=Werkzeuge+%26+Ressourcen+%28Gold%29&membership[membership_product_1_id]=44982&membership[membership_product_2]=Zertifizierung+zum%2Fr+Gradido-Botschafter%2Fin&membership[membership_product_2_id]=43970&membership[membership_product_3]=Seminar+3+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_3_id]=43969&membership[membership_product_4]=Potential-Entfaltungs-Techniken&membership[membership_product_4_id]=43954&membership[membership_product_5]=Seminar+2+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_5_id]=43896&membership[membership_product_6]=Kongresspaket%3A+Gesundes+Geld+f%C3%BCr+eine+gesunde+Welt&membership[membership_product_6_id]=14590&membership[membership_product_7]=Deine+Gold-Mitgliedschaft+bei+Gradido&membership[membership_product_7_id]=43951&membership[membership_product_8]=Gradido+E-Book%2C+H%C3%B6rspiel+%22Joytopia%22++%E2%80%93+und+100+Vorteile&membership[membership_product_8_id]=7312&membership[membership_product_9]=Danke%2C+dass+Du+hilfst+Gradido+in+die+Welt+zu+bringen%21&membership[membership_product_9_id]=43744&membership[membership_product_10]=Basis-Informationen+zu+Gradido&membership[membership_product_10_id]=42600&membership[membership_product_11]=Seminar+1+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_11_id]=43882&membership[membership_product_12]=Musical+%22Gradido+%E2%80%93+gemeinsam+retten+wir+die+Welt%22&membership[membership_product_12_id]=43886&membership[membership_product_13]=Premium+Community+%26+Markt&membership[membership_product_13_id]=43885&membership[membership_product_14]=Gradido+Buch+ungek%C3%BCrzte+Version+%26+%C3%9Cbersetzungen&membership[membership_product_14_id]=43887&membership[membership_product_15]=Online-Konferenzen&membership[membership_product_15_id]=43919&membership[membership_product_16]=Gradido+H%C3%B6rbuch&membership[membership_product_16_id]=43920&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&pricing_plan[name]=Monatlich&add_id_1&add_id_2&campaign_id¤cy=EUR&coupon_code&recurring=yes&recurring_form=subscription&payment_state=active_subscription&payment_method=sepa&opt_ins&payments_schedule[][rate]=1&payments_schedule[][state]=debt&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2019&payments_schedule[][rate]=2&payments_schedule[][state]=paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.11.2019&payments_schedule[][rate]=3&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.12.2019&payments_schedule[][rate]=4&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.01.2020&payments_schedule[][rate]=5&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.02.2020&payments_schedule[][rate]=6&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.03.2020&payments_schedule[][rate]=7&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.04.2020&payments_schedule[][rate]=8&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.05.2020&payments_schedule[][rate]=9&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.06.2020&payments_schedule[][rate]=10&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.07.2020&payments_schedule[][rate]=11&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.08.2020&payments_schedule[][rate]=12&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.09.2020&payments_schedule[][rate]=13&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2020&payments_schedule[][rate]=14&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.11.2020&payments_schedule[][rate]=15&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.12.2020&payments_count=1&payments_count_expected&with_test_period=false&with_custom_start=false&created=29.10.2019+13%3A17&id=57354055&invoice_number=111-1839-000000677&revenue=40.0&amount=23.72&fee=16.28&vat_rate=0.0&vat_amount=0.0&state=successful&refunded_transfer_id&invoice_link=http%3A%2F%2Felopage.com%2Fcommon%2Finvoices%2F450856%2Fdownload.pdf%3Ftoken%3DGR7bG7zcbgCzNJEPLDss&credit_memo_link&success_link=http%3A%2F%2Felopage.com%2Fs%2Fgradido%2Fpayment%2Fy22MJxHr9XzzPiaaH9GU&error_msg&created_date=2019-11-29T07%3A19Z&success_date=2019-12-06T13%3A12Z&success_date_short=2019-12-06&created_date_utc=29.11.2019+07%3A19&success_date_utc=06.12.2019+13%3A12&team_member_commissions&event=payment.successful + order_id=849951&order_token=y22MJxHr9XzzPiaaH9GU&payment_session_id=849951&payment_session_token=y22MJxHr9XzzPiaaH9GU&action=subscription_state_changed&initiator&payer[email]=theodora.mis%40gmx.ch&payer[first_name]=Theodora&payer[last_name]=Mis&payer[country]=Schweiz&payer[country_code]=CH&payer[city]=St.+Gallen&payer[street]=Vonwilstrasse+23&payer[street_number]&payer[zip]=9000&payer[company]&payer[vat_no]&payer[phone]&gift_receiver&publisher[id]=691&publisher[email]=joytopia%40gmail.com&publisher[first_name]=Bernd&publisher[last_name]=H%C3%BCckst%C3%A4dt&publisher[street]=Pfarrweg+2&publisher[zip]=74653&publisher[city]=K%C3%BCnzelsau&publisher[country]=Deutschland&publisher[phone]=%2B4979405460810&team_members&product_id=43944&product[id]=43944&product[slug]=gold-de&product[name]=Gold-Mitgliedschaft&product[type]=membership&product[price]=40.0&product[affiliate_program_id]=111&upsell&membership[id]=43944&membership[name]=Gold-Mitgliedschaft&membership[membership_product_1]=Werkzeuge+%26+Ressourcen+%28Gold%29&membership[membership_product_1_id]=44982&membership[membership_product_2]=Zertifizierung+zum%2Fr+Gradido-Botschafter%2Fin&membership[membership_product_2_id]=43970&membership[membership_product_3]=Seminar+3+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_3_id]=43969&membership[membership_product_4]=Potential-Entfaltungs-Techniken&membership[membership_product_4_id]=43954&membership[membership_product_5]=Seminar+2+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_5_id]=43896&membership[membership_product_6]=Kongresspaket%3A+Gesundes+Geld+f%C3%BCr+eine+gesunde+Welt&membership[membership_product_6_id]=14590&membership[membership_product_7]=Deine+Gold-Mitgliedschaft+bei+Gradido&membership[membership_product_7_id]=43951&membership[membership_product_8]=Gradido+E-Book%2C+H%C3%B6rspiel+%22Joytopia%22++%E2%80%93+und+100+Vorteile&membership[membership_product_8_id]=7312&membership[membership_product_9]=Danke%2C+dass+Du+hilfst+Gradido+in+die+Welt+zu+bringen%21&membership[membership_product_9_id]=43744&membership[membership_product_10]=Basis-Informationen+zu+Gradido&membership[membership_product_10_id]=42600&membership[membership_product_11]=Seminar+1+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_11_id]=43882&membership[membership_product_12]=Musical+%22Gradido+%E2%80%93+gemeinsam+retten+wir+die+Welt%22&membership[membership_product_12_id]=43886&membership[membership_product_13]=Premium+Community+%26+Markt&membership[membership_product_13_id]=43885&membership[membership_product_14]=Gradido+Buch+ungek%C3%BCrzte+Version+%26+%C3%9Cbersetzungen&membership[membership_product_14_id]=43887&membership[membership_product_15]=Online-Konferenzen&membership[membership_product_15_id]=43919&membership[membership_product_16]=Gradido+H%C3%B6rbuch&membership[membership_product_16_id]=43920&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&pricing_plan[name]=Monatlich&add_id_1&add_id_2&campaign_id¤cy=EUR&coupon_code&recurring=yes&recurring_form=subscription&payment_state=active_subscription&payment_method=sepa&opt_ins&payments_schedule[][rate]=1&payments_schedule[][state]=debt&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2019&payments_schedule[][rate]=2&payments_schedule[][state]=paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.11.2019&payments_schedule[][rate]=3&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.12.2019&payments_schedule[][rate]=4&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.01.2020&payments_schedule[][rate]=5&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.02.2020&payments_schedule[][rate]=6&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.03.2020&payments_schedule[][rate]=7&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.04.2020&payments_schedule[][rate]=8&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.05.2020&payments_schedule[][rate]=9&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.06.2020&payments_schedule[][rate]=10&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.07.2020&payments_schedule[][rate]=11&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.08.2020&payments_schedule[][rate]=12&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.09.2020&payments_schedule[][rate]=13&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2020&payments_schedule[][rate]=14&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.11.2020&payments_schedule[][rate]=15&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.12.2020&payments_count=1&payments_count_expected&with_test_period=false&with_custom_start=false&created=29.10.2019+13%3A17&event=order.subscription.paying + order_id=849951&order_token=y22MJxHr9XzzPiaaH9GU&payment_session_id=849951&payment_session_token=y22MJxHr9XzzPiaaH9GU&action=payment_processed&initiator&payer[email]=theodora.mis%40gmx.ch&payer[first_name]=Theodora&payer[last_name]=Mis&payer[country]=Schweiz&payer[country_code]=CH&payer[city]=St.+Gallen&payer[street]=Vonwilstrasse+23&payer[street_number]&payer[zip]=9000&payer[company]&payer[vat_no]&payer[phone]&gift_receiver&publisher[id]=691&publisher[email]=joytopia%40gmail.com&publisher[first_name]=Bernd&publisher[last_name]=H%C3%BCckst%C3%A4dt&publisher[street]=Pfarrweg+2&publisher[zip]=74653&publisher[city]=K%C3%BCnzelsau&publisher[country]=Deutschland&publisher[phone]=%2B4979405460810&team_members&product_id=43944&product[id]=43944&product[slug]=gold-de&product[name]=Gold-Mitgliedschaft&product[type]=membership&product[price]=40.0&product[affiliate_program_id]=111&upsell&membership[id]=43944&membership[name]=Gold-Mitgliedschaft&membership[membership_product_1]=Werkzeuge+%26+Ressourcen+%28Gold%29&membership[membership_product_1_id]=44982&membership[membership_product_2]=Zertifizierung+zum%2Fr+Gradido-Botschafter%2Fin&membership[membership_product_2_id]=43970&membership[membership_product_3]=Seminar+3+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_3_id]=43969&membership[membership_product_4]=Potential-Entfaltungs-Techniken&membership[membership_product_4_id]=43954&membership[membership_product_5]=Seminar+2+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_5_id]=43896&membership[membership_product_6]=Kongresspaket%3A+Gesundes+Geld+f%C3%BCr+eine+gesunde+Welt&membership[membership_product_6_id]=14590&membership[membership_product_7]=Deine+Gold-Mitgliedschaft+bei+Gradido&membership[membership_product_7_id]=43951&membership[membership_product_8]=Gradido+E-Book%2C+H%C3%B6rspiel+%22Joytopia%22++%E2%80%93+und+100+Vorteile&membership[membership_product_8_id]=7312&membership[membership_product_9]=Danke%2C+dass+Du+hilfst+Gradido+in+die+Welt+zu+bringen%21&membership[membership_product_9_id]=43744&membership[membership_product_10]=Basis-Informationen+zu+Gradido&membership[membership_product_10_id]=42600&membership[membership_product_11]=Seminar+1+Nat%C3%BCrliche+%C3%96konomie+des+Lebens&membership[membership_product_11_id]=43882&membership[membership_product_12]=Musical+%22Gradido+%E2%80%93+gemeinsam+retten+wir+die+Welt%22&membership[membership_product_12_id]=43886&membership[membership_product_13]=Premium+Community+%26+Markt&membership[membership_product_13_id]=43885&membership[membership_product_14]=Gradido+Buch+ungek%C3%BCrzte+Version+%26+%C3%9Cbersetzungen&membership[membership_product_14_id]=43887&membership[membership_product_15]=Online-Konferenzen&membership[membership_product_15_id]=43919&membership[membership_product_16]=Gradido+H%C3%B6rbuch&membership[membership_product_16_id]=43920&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&events[]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&tickets[][codes]&tickets[][ticket_attendees]&pricing_plan[name]=Monatlich&add_id_1&add_id_2&campaign_id¤cy=EUR&coupon_code&recurring=yes&recurring_form=subscription&payment_state=active_subscription&payment_method=sepa&opt_ins&payments_schedule[][rate]=1&payments_schedule[][state]=pending&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2019&payments_schedule[][rate]=2&payments_schedule[][state]=paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.11.2019&payments_schedule[][rate]=3&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.12.2019&payments_schedule[][rate]=4&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.01.2020&payments_schedule[][rate]=5&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.02.2020&payments_schedule[][rate]=6&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.03.2020&payments_schedule[][rate]=7&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.04.2020&payments_schedule[][rate]=8&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.05.2020&payments_schedule[][rate]=9&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.06.2020&payments_schedule[][rate]=10&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.07.2020&payments_schedule[][rate]=11&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.08.2020&payments_schedule[][rate]=12&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.09.2020&payments_schedule[][rate]=13&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=40.0&payments_schedule[][date]=29.10.2020&payments_schedule[][rate]=14&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.11.2020&payments_schedule[][rate]=15&payments_schedule[][state]=to_be_paid&payments_schedule[][amount]=0.0&payments_schedule[][date]=29.12.2020&payments_count=1&payments_count_expected&with_test_period=false&with_custom_start=false&created=29.10.2019+13%3A17&id=58838098&invoice_number=111-1839-000000689&revenue=40.0&amount=23.72&fee=16.28&vat_rate=0.0&vat_amount=0.0&state=pending&refunded_transfer_id&invoice_link=http%3A%2F%2Felopage.com%2Fcommon%2Finvoices%2F470009%2Fdownload.pdf%3Ftoken%3DZ_gogUf8tpKxcHhB-7Cz&credit_memo_link&success_link=http%3A%2F%2Felopage.com%2Fs%2Fgradido%2Fpayment%2Fy22MJxHr9XzzPiaaH9GU&error_msg&created_date=2019-12-07T07%3A19Z&success_date&success_date_short&created_date_utc=07.12.2019+07%3A19&success_date_utc&team_member_commissions&event=payment.pending + + Additional we have the Elopage API docu: + https://apidoc.elopage.com/#webhooks + + I assume that the webhook arrives via POST and transmits a string as shown above +*/ + +import { LoginElopageBuys } from '@entity/LoginElopageBuys' +import { LoginUser } from '@entity/LoginUser' +import { randomBytes } from 'crypto' +import { UserResolver } from '../graphql/resolver/UserResolver' + +export const elopageWebhook = async (req: any, res: any): Promise => { + res.status(200).end() // Responding is important + + const loginElopgaeBuy = new LoginElopageBuys() + let firstName = '' + let lastName = '' + const entries = req.body.split('&') + entries.foreach((entry: string) => { + const keyVal = entry.split('=') + if (keyVal.length !== 2) { + throw new Error(`Error parsing entry '${entry}'`) + } + const key = keyVal[0] + const val = decodeURIComponent(keyVal[1]).replace('+', ' ').trim() + switch (key) { + case 'product[affiliate_program_id]': + loginElopgaeBuy.affiliateProgramId = parseInt(val) + break + case 'publisher[id]': + loginElopgaeBuy.publisherId = parseInt(val) + break + case 'order_id': + loginElopgaeBuy.orderId = parseInt(val) + break + case 'product_id': + loginElopgaeBuy.productId = parseInt(val) + break + case 'product[price]': + // TODO: WHAT THE ACTUAL FUK? Please save this as float in the future directly in the database + loginElopgaeBuy.productPrice = Math.trunc(parseFloat(val) * 100) + break + case 'payer[email]': + loginElopgaeBuy.payerEmail = val + break + case 'publisher[email]': + loginElopgaeBuy.publisherEmail = val + break + case 'payment_state': + loginElopgaeBuy.payed = val === 'paid' + break + case 'success_date': + loginElopgaeBuy.successDate = new Date(val) + break + case 'event': + loginElopgaeBuy.event = val + break + case 'membership[id]': + // TODO this was never set on login_server - its unclear if this is the correct value + loginElopgaeBuy.elopageUserId = parseInt(val) + break + case 'payer[first_name]': + firstName = val + break + case 'payer[last_name]': + lastName = val + break + default: + // eslint-disable-next-line no-console + console.log(`Unknown Elopage Value '${entry}'`) + } + }) + + // Do not process certain events + if (['lesson.viewed', 'lesson.completed', 'lesson.commented'].includes(loginElopgaeBuy.event)) { + // eslint-disable-next-line no-console + console.log('User viewed, completed or commented - not saving hook') + return + } + + // Save the hook data + await loginElopgaeBuy.save() + + // create user for certain products + /* + Registrierung - Schritt 1 von 3, 36001 + Gradido-Basis, 43741 + Premium-Mitgliedschaft, 43870 + Gold-Mitgliedschaft, 43944 + Business-Mitgliedschaft, 43960 + Förderbeitrag: 49106 + */ + if ([36001, 43741, 43870, 43944, 43960, 49106].includes(loginElopgaeBuy.productId)) { + const email = loginElopgaeBuy.payerEmail + + const VALIDATE_EMAIL = /^[a-zA-Z0-9.!#$%&?*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/ + const VALIDATE_NAME = /^<>&;]{2,}$/ + + // Validate inputs + if ( + email === '' || + !email.match(VALIDATE_EMAIL) || + firstName === '' || + firstName.match(VALIDATE_NAME) || + lastName === '' || + lastName.match(VALIDATE_NAME) + ) { + // eslint-disable-next-line no-console + console.log(`Could not create User ${firstName} ${lastName} with email: ${email}`) + return + } + + // Do we already have such a user? + if ((await LoginUser.count({ email })) !== 0) { + // eslint-disable-next-line no-console + console.log(`Did not create User - already exists with email: ${email}`) + return + } + + const userResolver = new UserResolver() + try { + await userResolver.createUser({ + email, + firstName, + lastName, + publisherId: loginElopgaeBuy.publisherId, + }) + } catch (error) { + // eslint-disable-next-line no-console + console.log(`Could not create User for ${email}. Following Error occured:`, error) + } + } +} diff --git a/backend/yarn.lock b/backend/yarn.lock index b411bcf60..5b74ba7c3 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1552,7 +1552,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.19.0, body-parser@^1.18.3: +body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== diff --git a/frontend/.env.dist b/frontend/.env.dist index 8d4025a5d..80dafb7f9 100644 --- a/frontend/.env.dist +++ b/frontend/.env.dist @@ -1,3 +1,4 @@ GRAPHQL_URI=http://localhost:4000/graphql DEFAULT_PUBLISHER_ID=2896 -//BUILD_COMMIT=0000000 \ No newline at end of file +#BUILD_COMMIT=0000000 +ADMIN_AUTH_URL=http://localhost/admin/authenticate?token=$1 \ No newline at end of file diff --git a/frontend/DEV-README.md b/frontend/DEV-README.md deleted file mode 100644 index ab2b3e225..000000000 --- a/frontend/DEV-README.md +++ /dev/null @@ -1,21 +0,0 @@ -DEV README von Alex - -default Page: -´´´ - - - - -´´´ - diff --git a/frontend/ISSUE_TEMPLATE.md b/frontend/ISSUE_TEMPLATE.md deleted file mode 100644 index 8103f52e6..000000000 --- a/frontend/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/frontend/README.md b/frontend/README.md index e9ac0b097..f7c60552c 100755 --- a/frontend/README.md +++ b/frontend/README.md @@ -385,4 +385,13 @@ TODO: Update GDT-Server um paging und Zugriff auf alle Einträge zu erhalten, op GET https://staging.gradido.net/state-balances/ajaxGdtTransactions Liefert wenn alles in Ordnung ist: -wenn nicht type 7 dann "amount" in euro ansonsten in GDT \ No newline at end of file +wenn nicht type 7 dann "amount" in euro ansonsten in GDT + +## Additional Software + +For `yarn locales` you will need `jq` to use it. +You can install it (on arch) via + +``` +sudo pacman -S jq +``` \ No newline at end of file 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 c33c132b0..96882e816 100755 --- a/frontend/src/components/SidebarPlugin/SideBar.vue +++ b/frontend/src/components/SidebarPlugin/SideBar.vue @@ -45,11 +45,20 @@
+ + + @@ -112,6 +121,10 @@ export default { logout() { this.$emit('logout') }, + admin() { + window.location.assign(CONFIG.ADMIN_AUTH_URL.replace('$1', this.$store.state.token)) + this.$store.dispatch('logout') // logout without redirect + }, getElopageLink() { const pId = this.$store.state.publisherId ? this.$store.state.publisherId diff --git a/frontend/src/config/index.js b/frontend/src/config/index.js index 1f1819c62..b3a9366b7 100644 --- a/frontend/src/config/index.js +++ b/frontend/src/config/index.js @@ -18,8 +18,9 @@ const environment = { DEFAULT_PUBLISHER_ID: process.env.DEFAULT_PUBLISHER_ID || 2896, } -const server = { +const endpoints = { GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql', + ADMIN_AUTH_URL: process.env.ADMIN_AUTH_URL || 'http://localhost/admin/authenticate?token=$1', } const options = {} @@ -27,7 +28,7 @@ const options = {} const CONFIG = { ...version, ...environment, - ...server, + ...endpoints, ...options, } diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 957a3c30e..d867e14ad 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -15,6 +15,27 @@ export const login = gql` } hasElopage publisherId + isAdmin + } + } +` + +export const verifyLogin = gql` + query { + verifyLogin { + email + username + firstName + lastName + language + description + coinanimation + klickTipp { + newsletterState + } + hasElopage + publisherId + isAdmin } } ` diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index faa61886d..b0dfe36d4 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -1,4 +1,5 @@ { + "admin_area": "Adminbereich", "back": "Zurück", "community": { "choose-another-community": "Eine andere Gemeinschaft auswählen", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 91e25f61d..135729ffa 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -1,4 +1,5 @@ { + "admin_area": "Admin's area", "back": "Back", "community": { "choose-another-community": "Choose another community", 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 eebd6976e..dc8df4f13 100644 --- a/frontend/src/routes/guards.js +++ b/frontend/src/routes/guards.js @@ -1,12 +1,34 @@ -const addNavigationGuards = (router, store) => { +import { verifyLogin } from '../graphql/queries' + +const addNavigationGuards = (router, store, apollo) => { + // handle publisherId router.beforeEach((to, from, next) => { - // handle publisherId const publisherId = to.query.pid if (publisherId) { store.commit('publisherId', publisherId) delete to.query.pid } - // handle authentication + next() + }) + + // store token on authenticate + 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() + } + }) + + // handle authentication + router.beforeEach((to, from, next) => { if (to.meta.requiresAuth && !store.state.token) { next({ path: '/login' }) } else { diff --git a/frontend/src/routes/guards.test.js b/frontend/src/routes/guards.test.js index cf366eac8..f271c5427 100644 --- a/frontend/src/routes/guards.test.js +++ b/frontend/src/routes/guards.test.js @@ -30,7 +30,7 @@ describe('navigation guards', () => { }) describe('authorization', () => { - const navGuard = router.beforeHooks[0] + const navGuard = router.beforeHooks[2] const next = jest.fn() it('redirects to login when not authorized', () => { diff --git a/frontend/src/routes/router.test.js b/frontend/src/routes/router.test.js index df4f9c229..cd26b6f6b 100644 --- a/frontend/src/routes/router.test.js +++ b/frontend/src/routes/router.test.js @@ -49,8 +49,8 @@ describe('router', () => { expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' }) }) - it('has fourteen routes defined', () => { - expect(routes).toHaveLength(14) + it('has fifteen routes defined', () => { + expect(routes).toHaveLength(15) }) describe('overview', () => { diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index 8f716ff72..9f7895859 100755 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -1,6 +1,9 @@ import NotFound from '@/views/NotFoundPage.vue' const routes = [ + { + path: '/authenticate', + }, { path: '/', redirect: (to) => { diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 6a229c161..c49197059 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -34,6 +34,9 @@ export const mutations = { if (isNaN(pubId)) pubId = null state.publisherId = pubId }, + isAdmin: (state, isAdmin) => { + state.isAdmin = !!isAdmin + }, community: (state, community) => { state.community = community }, @@ -57,6 +60,7 @@ export const actions = { commit('newsletterState', data.klickTipp.newsletterState) commit('hasElopage', data.hasElopage) commit('publisherId', data.publisherId) + commit('isAdmin', data.isAdmin) }, logout: ({ commit, state }) => { commit('token', null) @@ -69,6 +73,7 @@ export const actions = { commit('newsletterState', null) commit('hasElopage', false) commit('publisherId', null) + commit('isAdmin', false) localStorage.clear() }, } @@ -87,6 +92,7 @@ export const store = new Vuex.Store({ username: '', description: '', token: null, + isAdmin: false, coinanimation: true, newsletterState: null, community: { diff --git a/frontend/src/store/store.test.js b/frontend/src/store/store.test.js index bdb98d03b..829678b44 100644 --- a/frontend/src/store/store.test.js +++ b/frontend/src/store/store.test.js @@ -148,11 +148,12 @@ describe('Vuex store', () => { }, hasElopage: false, publisherId: 1234, + isAdmin: true, } - it('calls ten commits', () => { + it('calls eleven commits', () => { login({ commit, state }, commitedData) - expect(commit).toHaveBeenCalledTimes(10) + expect(commit).toHaveBeenCalledTimes(11) }) it('commits email', () => { @@ -204,15 +205,20 @@ describe('Vuex store', () => { login({ commit, state }, commitedData) expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', 1234) }) + + it('commits isAdmin', () => { + login({ commit, state }, commitedData) + expect(commit).toHaveBeenNthCalledWith(11, 'isAdmin', true) + }) }) describe('logout', () => { const commit = jest.fn() const state = {} - it('calls ten commits', () => { + it('calls eleven commits', () => { logout({ commit, state }) - expect(commit).toHaveBeenCalledTimes(10) + expect(commit).toHaveBeenCalledTimes(11) }) it('commits token', () => { @@ -265,6 +271,11 @@ describe('Vuex store', () => { expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', null) }) + it('commits isAdmin', () => { + logout({ commit, state }) + expect(commit).toHaveBeenNthCalledWith(11, 'isAdmin', false) + }) + // how to get this working? it.skip('calls localStorage.clear()', () => { const clearStorageMock = jest.fn()