diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 902b71b11..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: 65
+ min_coverage: 51
token: ${{ github.token }}
##############################################################################
@@ -491,7 +491,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
- min_coverage: 38
+ min_coverage: 37
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/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 70bc2978a..40460eda4 100644
--- a/admin/src/App.vue
+++ b/admin/src/App.vue
@@ -1,19 +1,15 @@
-
-
-
+
+
diff --git a/admin/src/components/Footer.vue b/admin/src/components/ContentFooter.vue
similarity index 75%
rename from admin/src/components/Footer.vue
rename to admin/src/components/ContentFooter.vue
index 398f2e180..ade4e0a83 100644
--- a/admin/src/components/Footer.vue
+++ b/admin/src/components/ContentFooter.vue
@@ -8,3 +8,8 @@
+
diff --git a/admin/src/components/CreationFormular.spec.js b/admin/src/components/CreationFormular.spec.js
index f218cf8e2..fcdf97cfa 100644
--- a/admin/src/components/CreationFormular.spec.js
+++ b/admin/src/components/CreationFormular.spec.js
@@ -19,7 +19,7 @@ const mocks = {
const propsData = {
type: '',
item: {},
- creation: {},
+ creation: [],
itemsMassCreation: {},
}
@@ -38,5 +38,104 @@ describe('CreationFormular', () => {
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
index 9334f5d00..d6b637152 100644
--- a/admin/src/components/CreationFormular.vue
+++ b/admin/src/components/CreationFormular.vue
@@ -146,7 +146,7 @@ export default {
required: false,
},
creation: {
- type: Object,
+ type: Array,
required: true,
},
itemsMassCreation: {
@@ -198,9 +198,10 @@ export default {
this.text = this.creationUserData.text
break
case 'range':
- this.value = this.creationUserData.creation_gdd
+ this.value = this.creationUserData.creationGdd
break
default:
+ // TODO: Toast
alert("I don't know such values")
}
},
@@ -262,9 +263,11 @@ export default {
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
alert('UPDATE EINZEL SCHÖPFUNG ABSENDEN FÜR >> ')
// umschreiben, update eine bestehende Schöpfung eine
- this.creationUserData.datum = this.radioSelected.long
- this.creationUserData.creation_gdd = this.value
- this.creationUserData.text = this.text
+ this.$emit('update-creation-data', {
+ datum: this.radioSelected.long,
+ creationGdd: this.value,
+ text: this.text,
+ })
} else {
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
alert('EINZEL SCHÖPFUNG ABSENDEN FÜR >> ' + this.item.firstName + '')
diff --git a/admin/src/components/NavBar.vue b/admin/src/components/NavBar.vue
index de9cfe6b2..c52743857 100644
--- a/admin/src/components/NavBar.vue
+++ b/admin/src/components/NavBar.vue
@@ -16,15 +16,43 @@
>
| {{ $store.state.openCreations }} offene Schöpfungen
+ Wallet
+ Logout
- Profilbereich
diff --git a/admin/src/components/UserTable.spec.js b/admin/src/components/UserTable.spec.js
index a87497d81..3db0131a3 100644
--- a/admin/src/components/UserTable.spec.js
+++ b/admin/src/components/UserTable.spec.js
@@ -10,7 +10,7 @@ describe('UserTable', () => {
type: 'Type',
itemsUser: [],
fieldsTable: [],
- creation: {},
+ creation: [],
}
const Wrapper = () => {
diff --git a/admin/src/components/UserTable.vue b/admin/src/components/UserTable.vue
index 92ee0cba0..265c2d12e 100644
--- a/admin/src/components/UserTable.vue
+++ b/admin/src/components/UserTable.vue
@@ -67,6 +67,7 @@
:creation="row.item.creation"
:item="row.item"
:creationUserData="creationData"
+ @update-creation-data="updateCreationData"
/>
@@ -139,7 +140,7 @@ export default {
default: '',
},
creation: {
- type: Object,
+ type: Array,
required: false,
},
},
@@ -226,6 +227,11 @@ export default {
}
row.toggleDetails()
},
+ updateCreationData(data) {
+ this.creationData = {
+ ...data,
+ }
+ },
},
}
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/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/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/views/Creation.vue b/admin/src/pages/Creation.vue
similarity index 93%
rename from admin/src/views/Creation.vue
rename to admin/src/pages/Creation.vue
index df5bea28c..7ab900b43 100644
--- a/admin/src/views/Creation.vue
+++ b/admin/src/pages/Creation.vue
@@ -1,5 +1,5 @@
-
+
@@ -10,6 +10,7 @@
placeholder="User suche"
>
{
return {
...user,
- // showDetails: true,
+ showDetails: false,
}
})
})
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/views/CreationConfirm.vue b/admin/src/pages/CreationConfirm.vue
similarity index 98%
rename from admin/src/views/CreationConfirm.vue
rename to admin/src/pages/CreationConfirm.vue
index 83ba54552..0d68635e0 100644
--- a/admin/src/views/CreationConfirm.vue
+++ b/admin/src/pages/CreationConfirm.vue
@@ -1,5 +1,5 @@
-
+
Die anzahl der offene Schöpfungen stimmen nicht! Diese wird bei absenden im $store
hochgezählt. Die Liste die hier angezeigt wird ist SIMULIERT!
@@ -17,7 +17,7 @@
import UserTable from '../components/UserTable.vue'
export default {
- name: 'creation_confirm',
+ name: 'CreationConfirm',
components: {
UserTable,
},
diff --git a/admin/src/views/Overview.vue b/admin/src/pages/Overview.vue
similarity index 100%
rename from admin/src/views/Overview.vue
rename to admin/src/pages/Overview.vue
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/views/UserSearch.vue b/admin/src/pages/UserSearch.vue
similarity index 96%
rename from admin/src/views/UserSearch.vue
rename to admin/src/pages/UserSearch.vue
index 0172d6a23..ae0ade7b2 100644
--- a/admin/src/views/UserSearch.vue
+++ b/admin/src/pages/UserSearch.vue
@@ -1,5 +1,5 @@
-
+
{
+ // 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 a13463e08..72e7b1ac5 100644
--- a/admin/src/router/routes.js
+++ b/admin/src/router/routes.js
@@ -1,38 +1,27 @@
const routes = [
{
- path: '/',
- component: () => import('@/views/Overview.vue'),
- meta: {
- requiresAuth: true,
- },
+ path: '/authenticate',
},
{
- path: '/overview',
- component: () => import('@/views/Overview.vue'),
- meta: {
- requiresAuth: true,
- },
+ 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('@/views/UserSearch.vue'),
- meta: {
- requiresAuth: true,
- },
+ component: () => import('@/pages/UserSearch.vue'),
},
{
path: '/creation',
- component: () => import('@/views/Creation.vue'),
- meta: {
- requiresAuth: true,
- },
+ component: () => import('@/pages/Creation.vue'),
},
{
path: '/creation-confirm',
- component: () => import('@/views/CreationConfirm.vue'),
- meta: {
- requiresAuth: true,
- },
+ component: () => import('@/pages/CreationConfirm.vue'),
},
{
path: '*',
diff --git a/admin/src/store/store.js b/admin/src/store/store.js
index 38a210fe1..754c559c8 100644
--- a/admin/src/store/store.js
+++ b/admin/src/store/store.js
@@ -1,15 +1,16 @@
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 = state.openCreations + i
+ state.openCreations += i
},
openCreationsMinus: (state, i) => {
- state.openCreations = state.openCreations - i
+ state.openCreations -= i
},
resetOpenCreations: (state) => {
state.openCreations = 0
@@ -19,6 +20,13 @@ export const mutations = {
},
}
+export const actions = {
+ logout: ({ commit, state }) => {
+ commit('token', null)
+ window.localStorage.clear()
+ },
+}
+
const store = new Vuex.Store({
plugins: [
createPersistedState({
@@ -26,12 +34,13 @@ const store = new Vuex.Store({
}),
],
state: {
- token: 'some-valid-token',
+ 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/backend/.env.dist b/backend/.env.dist
index b4a91026a..1b485b8e4 100644
--- a/backend/.env.dist
+++ b/backend/.env.dist
@@ -30,4 +30,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 7a55de8e8..eab1b4608 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -55,9 +55,21 @@ const email = {
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$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 3d09e56eb..0d63e76bb 100644
--- a/backend/src/graphql/arg/CreateUserArgs.ts
+++ b/backend/src/graphql/arg/CreateUserArgs.ts
@@ -12,10 +12,7 @@ export default class CreateUserArgs {
lastName: string
@Field(() => String)
- password: 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/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts
index fffd1f7e6..235b6c90f 100644
--- a/backend/src/graphql/resolver/UserResolver.ts
+++ b/backend/src/graphql/resolver/UserResolver.ts
@@ -194,6 +194,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(
@@ -266,6 +329,7 @@ export class UserResolver {
throw new Error(error)
})
user.coinanimation = coinanimation
+ user.isAdmin = true // TODO implement
context.setHeaders.push({
key: 'token',
@@ -303,22 +367,23 @@ export class UserResolver {
@Mutation(() => String)
async createUser(
- @Args() { email, firstName, lastName, password, language, publisherId }: CreateUserArgs,
+ @Args() { email, firstName, lastName, language, publisherId }: CreateUserArgs,
): Promise {
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
// Validate Language (no throw)
- if (!isLanguage(language)) {
+ if (!language || !isLanguage(language)) {
language = DEFAULT_LANGUAGE
}
+ // TODO: Register process
// Validate Password
- if (!isPassword(password)) {
- throw new Error(
- 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
- )
- }
+ // if (!isPassword(password)) {
+ // throw new Error(
+ // 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
+ // )
+ // }
// Validate username
// TODO: never true
@@ -336,11 +401,13 @@ export class UserResolver {
throw new Error(`User already exists.`)
}
- const passphrase = PassphraseGenerate()
- const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
- const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
+ // TODO: Register process
+ // const passphrase = PassphraseGenerate()
+ // const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
+ // const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
+ // const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
+
const emailHash = getEmailHash(email)
- const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
// Table: login_users
const loginUser = new LoginUser()
@@ -349,13 +416,15 @@ export class UserResolver {
loginUser.lastName = lastName
loginUser.username = username
loginUser.description = ''
- loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
+ // TODO: Register process
+ // loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.emailHash = emailHash
loginUser.language = language
loginUser.groupId = 1
loginUser.publisherId = publisherId
- loginUser.pubKey = keyPair[0]
- loginUser.privKey = encryptedPrivkey
+ // TODO: Register process
+ // loginUser.pubKey = keyPair[0]
+ // loginUser.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
@@ -367,21 +436,24 @@ export class UserResolver {
throw new Error('insert user failed')
})
+ // TODO: Register process
// Table: login_user_backups
- const loginUserBackup = new LoginUserBackup()
- loginUserBackup.userId = loginUserId
- loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
- loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
+ // const loginUserBackup = new LoginUserBackup()
+ // loginUserBackup.userId = loginUserId
+ // loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
+ // loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
- await queryRunner.manager.save(loginUserBackup).catch((error) => {
- // eslint-disable-next-line no-console
- console.log('insert LoginUserBackup failed', error)
- throw new Error('insert user backup failed')
- })
+ // TODO: Register process
+ // await queryRunner.manager.save(loginUserBackup).catch((error) => {
+ // // eslint-disable-next-line no-console
+ // console.log('insert LoginUserBackup failed', error)
+ // throw new Error('insert user backup failed')
+ // })
// Table: state_users
const dbUser = new DbUser()
- dbUser.pubkey = keyPair[0]
+ // TODO: Register process
+ // dbUser.pubkey = keyPair[0]
dbUser.email = email
dbUser.firstName = firstName
dbUser.lastName = lastName
diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts
index d5fbc20ee..e5dfd113b 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/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/docker-compose.override.yml b/docker-compose.override.yml
index 9f3d3d618..83f38a95f 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -1,166 +1,166 @@
-version: "3.4"
-
-services:
- ########################################################
- # FRONTEND #############################################
- ########################################################
- frontend:
- image: gradido/frontend:development
- build:
- target: development
- environment:
- - NODE_ENV="development"
- # - DEBUG=true
- volumes:
- # This makes sure the docker container has its own node modules.
- # Therefore it is possible to have a different node version on the host machine
- - frontend_node_modules:/app/node_modules
- # bind the local folder to the docker to allow live reload
- - ./frontend:/app
-
- ########################################################
- # ADMIN INTERFACE ######################################
- ########################################################
- admin:
- image: gradido/admin:development
- build:
- target: development
- environment:
- - NODE_ENV="development"
- # - DEBUG=true
- volumes:
- # This makes sure the docker container has its own node modules.
- # Therefore it is possible to have a different node version on the host machine
- - admin_node_modules:/app/node_modules
- # bind the local folder to the docker to allow live reload
- - ./admin:/app
-
- ########################################################
- # BACKEND ##############################################
- ########################################################
- backend:
- image: gradido/backend:development
- build:
- target: development
- networks:
- - external-net
- - internal-net
- environment:
- - NODE_ENV="development"
- volumes:
- # This makes sure the docker container has its own node modules.
- # Therefore it is possible to have a different node version on the host machine
- - backend_node_modules:/app/node_modules
- - backend_database_node_modules:/database/node_modules
- - backend_database_build:/database/build
- # bind the local folder to the docker to allow live reload
- - ./backend:/app
- - ./database:/database
-
- ########################################################
- # DATABASE ##############################################
- ########################################################
- database:
- # we always run on production here since else the service lingers
- # feel free to change this behaviour if it seems useful
- # Due to problems with the volume caching the built files
- # we changed this to test build. This keeps the service running.
- image: gradido/database:test_up
- build:
- target: test_up
- #networks:
- # - external-net
- # - internal-net
- environment:
- - NODE_ENV="development"
- volumes:
- # This makes sure the docker container has its own node modules.
- # Therefore it is possible to have a different node version on the host machine
- - database_node_modules:/app/node_modules
- - database_build:/app/build
- # bind the local folder to the docker to allow live reload
- - ./database:/app
-
- #########################################################
- ## LOGIN SERVER #########################################
- #########################################################
- login-server:
- build:
- dockerfile: Dockerfiles/ubuntu/Dockerfile.debug
- networks:
- - external-net
- - internal-net
- security_opt:
- - seccomp:unconfined
- cap_add:
- - SYS_PTRACE
- volumes:
- - ./logs:/var/log/grd_login
- - ./login_server/src:/code/src
- - ./login_server/dependencies:/code/dependencies
- - ./login_server/scripts:/code/scripts
- - ./configs/login_server:/etc/grd_login
- - login_build_ubuntu_3.1:/code/build
-
-
- #########################################################
- ## COMMUNITY SERVER (cakephp with php-fpm) ##############
- #########################################################
- community-server:
- build:
- context: .
- target: community_server
- dockerfile: ./community_server/Dockerfile
- depends_on:
- - mariadb
- networks:
- - internal-net
- - external-net
- volumes:
- - ./community_server/config/php-fpm/php-ini-overrides.ini:/etc/php/7.4/fpm/conf.d/99-overrides.ini
- - ./community_server/src:/var/www/cakephp/src
-
- #########################################################
- ## MARIADB ##############################################
- #########################################################
- mariadb:
- networks:
- - internal-net
- - external-net
-
- #########################################################
- ## NGINX ################################################
- #########################################################
- nginx:
- depends_on:
- - frontend
- - community-server
- - login-server
- volumes:
- - ./logs/nginx:/var/log/nginx
-
- #########################################################
- ## PHPMYADMIN ###########################################
- #########################################################
- phpmyadmin:
- image: phpmyadmin
- environment:
- - PMA_ARBITRARY=1
- #restart: always
- ports:
- - 8074:80
- networks:
- - internal-net
- - external-net
- volumes:
- - /sessions
-
-volumes:
- frontend_node_modules:
- admin_node_modules:
- backend_node_modules:
- backend_database_node_modules:
- backend_database_build:
- database_node_modules:
- database_build:
+version: "3.4"
+
+services:
+ ########################################################
+ # FRONTEND #############################################
+ ########################################################
+ frontend:
+ image: gradido/frontend:development
+ build:
+ target: development
+ environment:
+ - NODE_ENV="development"
+ # - DEBUG=true
+ volumes:
+ # This makes sure the docker container has its own node modules.
+ # Therefore it is possible to have a different node version on the host machine
+ - frontend_node_modules:/app/node_modules
+ # bind the local folder to the docker to allow live reload
+ - ./frontend:/app
+
+ ########################################################
+ # ADMIN INTERFACE ######################################
+ ########################################################
+ admin:
+ image: gradido/admin:development
+ build:
+ target: development
+ environment:
+ - NODE_ENV="development"
+ # - DEBUG=true
+ volumes:
+ # This makes sure the docker container has its own node modules.
+ # Therefore it is possible to have a different node version on the host machine
+ - admin_node_modules:/app/node_modules
+ # bind the local folder to the docker to allow live reload
+ - ./admin:/app
+
+ ########################################################
+ # BACKEND ##############################################
+ ########################################################
+ backend:
+ image: gradido/backend:development
+ build:
+ target: development
+ networks:
+ - external-net
+ - internal-net
+ environment:
+ - NODE_ENV="development"
+ volumes:
+ # This makes sure the docker container has its own node modules.
+ # Therefore it is possible to have a different node version on the host machine
+ - backend_node_modules:/app/node_modules
+ - backend_database_node_modules:/database/node_modules
+ - backend_database_build:/database/build
+ # bind the local folder to the docker to allow live reload
+ - ./backend:/app
+ - ./database:/database
+
+ ########################################################
+ # DATABASE ##############################################
+ ########################################################
+ database:
+ # we always run on production here since else the service lingers
+ # feel free to change this behaviour if it seems useful
+ # Due to problems with the volume caching the built files
+ # we changed this to test build. This keeps the service running.
+ image: gradido/database:test_up
+ build:
+ target: test_up
+ #networks:
+ # - external-net
+ # - internal-net
+ environment:
+ - NODE_ENV="development"
+ volumes:
+ # This makes sure the docker container has its own node modules.
+ # Therefore it is possible to have a different node version on the host machine
+ - database_node_modules:/app/node_modules
+ - database_build:/app/build
+ # bind the local folder to the docker to allow live reload
+ - ./database:/app
+
+ #########################################################
+ ## LOGIN SERVER #########################################
+ #########################################################
+ login-server:
+ build:
+ dockerfile: Dockerfiles/ubuntu/Dockerfile.debug
+ networks:
+ - external-net
+ - internal-net
+ security_opt:
+ - seccomp:unconfined
+ cap_add:
+ - SYS_PTRACE
+ volumes:
+ - ./logs:/var/log/grd_login
+ - ./login_server/src:/code/src
+ - ./login_server/dependencies:/code/dependencies
+ - ./login_server/scripts:/code/scripts
+ - ./configs/login_server:/etc/grd_login
+ - login_build_ubuntu_3.1:/code/build
+
+
+ #########################################################
+ ## COMMUNITY SERVER (cakephp with php-fpm) ##############
+ #########################################################
+ community-server:
+ build:
+ context: .
+ target: community_server
+ dockerfile: ./community_server/Dockerfile
+ depends_on:
+ - mariadb
+ networks:
+ - internal-net
+ - external-net
+ volumes:
+ - ./community_server/config/php-fpm/php-ini-overrides.ini:/etc/php/7.4/fpm/conf.d/99-overrides.ini
+ - ./community_server/src:/var/www/cakephp/src
+
+ #########################################################
+ ## MARIADB ##############################################
+ #########################################################
+ mariadb:
+ networks:
+ - internal-net
+ - external-net
+
+ #########################################################
+ ## NGINX ################################################
+ #########################################################
+ nginx:
+ depends_on:
+ - frontend
+ - community-server
+ - login-server
+ volumes:
+ - ./logs/nginx:/var/log/nginx
+
+ #########################################################
+ ## PHPMYADMIN ###########################################
+ #########################################################
+ phpmyadmin:
+ image: phpmyadmin
+ environment:
+ - PMA_ARBITRARY=1
+ #restart: always
+ ports:
+ - 8074:80
+ networks:
+ - internal-net
+ - external-net
+ volumes:
+ - /sessions
+
+volumes:
+ frontend_node_modules:
+ admin_node_modules:
+ backend_node_modules:
+ backend_database_node_modules:
+ backend_database_build:
+ database_node_modules:
+ database_build:
login_build_ubuntu_3.1:
\ No newline at end of file
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:
-´´´
-
- default
-
-
-
-
-´´´
-
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 01021f601..8b55f4098 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 a3c1389ce..f6975d09d 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()