mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into login_call_resetPassword
This commit is contained in:
commit
5cbdbcb7c9
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -441,7 +441,7 @@ jobs:
|
|||||||
report_name: Coverage Admin Interface
|
report_name: Coverage Admin Interface
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./coverage/lcov.info
|
result_path: ./coverage/lcov.info
|
||||||
min_coverage: 65
|
min_coverage: 51
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|||||||
3
admin/.env.dist
Normal file
3
admin/.env.dist
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
GRAPHQL_URI=http://localhost:4000/graphql
|
||||||
|
WALLET_AUTH_URL=http://localhost/vue/authenticate?token=$1
|
||||||
|
DEBUG_DISABLE_AUTH=false
|
||||||
@ -1,6 +1,11 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
verbose: true,
|
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: [
|
moduleFileExtensions: [
|
||||||
'js',
|
'js',
|
||||||
// 'jsx',
|
// 'jsx',
|
||||||
|
|||||||
@ -32,15 +32,19 @@
|
|||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"dotenv-webpack": "^7.0.3",
|
"dotenv-webpack": "^7.0.3",
|
||||||
"graphql": "^15.6.1",
|
"graphql": "^15.6.1",
|
||||||
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "26.6.3",
|
"jest": "26.6.3",
|
||||||
|
"moment": "^2.29.1",
|
||||||
"regenerator-runtime": "^0.13.9",
|
"regenerator-runtime": "^0.13.9",
|
||||||
"stats-webpack-plugin": "^0.7.0",
|
"stats-webpack-plugin": "^0.7.0",
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-apollo": "^3.0.8",
|
"vue-apollo": "^3.0.8",
|
||||||
"vue-i18n": "^8.26.5",
|
"vue-i18n": "^8.26.5",
|
||||||
"vue-jest": "^3.0.7",
|
"vue-jest": "^3.0.7",
|
||||||
|
"vue-moment": "^4.1.0",
|
||||||
"vue-router": "^3.5.3",
|
"vue-router": "^3.5.3",
|
||||||
"vuex": "^3.6.2"
|
"vuex": "^3.6.2",
|
||||||
|
"vuex-persistedstate": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "^7.15.8",
|
"@babel/eslint-parser": "^7.15.8",
|
||||||
|
|||||||
@ -1,43 +1,28 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { shallowMount } from '@vue/test-utils'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
const storeCommitMock = jest.fn()
|
const stubs = {
|
||||||
|
RouterView: true,
|
||||||
|
}
|
||||||
|
|
||||||
const mocks = {
|
const mocks = {
|
||||||
$store: {
|
$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', () => {
|
describe('App', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(App, { localVue, mocks })
|
return shallowMount(App, { localVue, stubs, mocks })
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('shallowMount', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = Wrapper()
|
wrapper = Wrapper()
|
||||||
})
|
})
|
||||||
@ -46,23 +31,4 @@ describe('App', () => {
|
|||||||
expect(wrapper.find('div#app').exists()).toBeTruthy()
|
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app"></div>
|
<div id="app">
|
||||||
|
<default-layout v-if="$store.state.token" />
|
||||||
|
<router-view v-else></router-view>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import defaultLayout from '@/layouts/defaultLayout.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'app',
|
||||||
|
components: { defaultLayout },
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
1
admin/src/assets/mocks/styleMock.js
Normal file
1
admin/src/assets/mocks/styleMock.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
module.exports = {}
|
||||||
15
admin/src/components/ContentFooter.vue
Normal file
15
admin/src/components/ContentFooter.vue
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<hr />
|
||||||
|
<br />
|
||||||
|
<div class="text-center">
|
||||||
|
Gradido Akademie Adminkonsole
|
||||||
|
<div><small>Version: 0.0.1</small></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ContentFooter',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
141
admin/src/components/CreationFormular.spec.js
Normal file
141
admin/src/components/CreationFormular.spec.js
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import CreationFormular from './CreationFormular.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$moment: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
format: jest.fn((m) => m),
|
||||||
|
subtract: jest.fn(() => {
|
||||||
|
return {
|
||||||
|
format: jest.fn((m) => m),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
type: '',
|
||||||
|
item: {},
|
||||||
|
creation: [],
|
||||||
|
itemsMassCreation: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CreationFormular', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(CreationFormular, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV element with the class.component-creation-formular', () => {
|
||||||
|
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('radio buttons to selcet month', () => {
|
||||||
|
it('has three radio buttons', () => {
|
||||||
|
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with mass creation', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await wrapper.setProps({ type: 'massCreation' })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('first radio button', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits update-radio-selected with index 0', () => {
|
||||||
|
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||||
|
[expect.arrayContaining([0])],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('second radio button', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits update-radio-selected with index 1', () => {
|
||||||
|
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||||
|
[expect.arrayContaining([1])],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('third radio button', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits update-radio-selected with index 2', () => {
|
||||||
|
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||||
|
[expect.arrayContaining([2])],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with single creation', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
|
||||||
|
await wrapper.setData({ rangeMin: 180 })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('first radio button', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets rangeMin to 0', () => {
|
||||||
|
expect(wrapper.vm.rangeMin).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets rangeMax to 200', () => {
|
||||||
|
expect(wrapper.vm.rangeMax).toBe(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('second radio button', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets rangeMin to 0', () => {
|
||||||
|
expect(wrapper.vm.rangeMin).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets rangeMax to 400', () => {
|
||||||
|
expect(wrapper.vm.rangeMax).toBe(400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('third radio button', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets rangeMin to 0', () => {
|
||||||
|
expect(wrapper.vm.rangeMin).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets rangeMax to 400', () => {
|
||||||
|
expect(wrapper.vm.rangeMax).toBe(600)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
290
admin/src/components/CreationFormular.vue
Normal file
290
admin/src/components/CreationFormular.vue
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<div class="component-creation-formular">
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{{
|
||||||
|
this.type === 'singleCreation'
|
||||||
|
? 'Einzelschöpfung für ' + item.firstName + ' ' + item.lastName + ''
|
||||||
|
: 'Mehrfachschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder'
|
||||||
|
}}
|
||||||
|
{{ item }}
|
||||||
|
</h3>
|
||||||
|
<div v-show="this.type === 'massCreation' && Object.keys(this.itemsMassCreation).length <= 0">
|
||||||
|
Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="this.type === 'singleCreation' || Object.keys(this.itemsMassCreation).length > 0"
|
||||||
|
class="shadow p-3 mb-5 bg-white rounded"
|
||||||
|
>
|
||||||
|
<b-form ref="creationForm">
|
||||||
|
<b-row class="m-4">
|
||||||
|
<label>Monat Auswählen</label>
|
||||||
|
<b-col class="text-left">
|
||||||
|
<b-form-radio
|
||||||
|
v-model="radioSelected"
|
||||||
|
:value="beforeLastMonth"
|
||||||
|
size="lg"
|
||||||
|
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
|
||||||
|
>
|
||||||
|
{{ beforeLastMonth.short }} {{ creation[0] != null ? creation[0] + ' GDD' : '' }}
|
||||||
|
</b-form-radio>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-form-radio
|
||||||
|
v-model="radioSelected"
|
||||||
|
:value="lastMonth"
|
||||||
|
size="lg"
|
||||||
|
@change="updateRadioSelected(lastMonth, 1, creation[1])"
|
||||||
|
>
|
||||||
|
{{ lastMonth.short }} {{ creation[1] != null ? creation[1] + ' GDD' : '' }}
|
||||||
|
</b-form-radio>
|
||||||
|
</b-col>
|
||||||
|
<b-col class="text-right">
|
||||||
|
<b-form-radio
|
||||||
|
v-model="radioSelected"
|
||||||
|
:value="currentMonth"
|
||||||
|
size="lg"
|
||||||
|
@change="updateRadioSelected(currentMonth, 2, creation[2])"
|
||||||
|
>
|
||||||
|
{{ currentMonth.short }} {{ creation[2] != null ? creation[2] + ' GDD' : '' }}
|
||||||
|
</b-form-radio>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<b-row class="m-4">
|
||||||
|
<label>Betrag Auswählen</label>
|
||||||
|
<b-input-group>
|
||||||
|
<template #append>
|
||||||
|
<b-input-group-text><strong class="text-danger">GDD</strong></b-input-group-text>
|
||||||
|
</template>
|
||||||
|
<b-form-input
|
||||||
|
type="number"
|
||||||
|
v-model="value"
|
||||||
|
:min="rangeMin"
|
||||||
|
:max="rangeMax"
|
||||||
|
></b-form-input>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
<b-input
|
||||||
|
id="range-2"
|
||||||
|
class="mt-2"
|
||||||
|
v-model="value"
|
||||||
|
type="range"
|
||||||
|
:min="rangeMin"
|
||||||
|
:max="rangeMax"
|
||||||
|
step="10"
|
||||||
|
@load="checkFormForUpdate('range')"
|
||||||
|
></b-input>
|
||||||
|
</b-row>
|
||||||
|
<b-row class="m-4">
|
||||||
|
<label>Text eintragen</label>
|
||||||
|
<div>
|
||||||
|
<b-form-textarea
|
||||||
|
id="textarea-state"
|
||||||
|
v-model="text"
|
||||||
|
:state="text.length >= 10"
|
||||||
|
placeholder="Mindestens 10 Zeichen eingeben"
|
||||||
|
@load="checkFormForUpdate('text')"
|
||||||
|
rows="3"
|
||||||
|
></b-form-textarea>
|
||||||
|
</div>
|
||||||
|
</b-row>
|
||||||
|
<b-row class="m-4">
|
||||||
|
<b-col class="text-center">
|
||||||
|
<b-button type="reset" variant="danger" @click="$refs.creationForm.reset()">
|
||||||
|
zurücksetzen
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
<b-col class="text-center">
|
||||||
|
<div class="text-right">
|
||||||
|
<b-button
|
||||||
|
v-if="pagetype === 'PageCreationConfirm'"
|
||||||
|
type="button"
|
||||||
|
variant="success"
|
||||||
|
@click="submitCreation"
|
||||||
|
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||||
|
>
|
||||||
|
Update Schöpfung ({{ type }},{{ pagetype }})
|
||||||
|
</b-button>
|
||||||
|
|
||||||
|
<b-button
|
||||||
|
v-else
|
||||||
|
type="button"
|
||||||
|
variant="success"
|
||||||
|
@click="submitCreation"
|
||||||
|
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||||
|
>
|
||||||
|
Schöpfung einreichen ({{ type }})
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CreationFormular',
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
pagetype: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
creationUserData: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
creation: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
itemsMassCreation: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
radioSelected: '',
|
||||||
|
text: '',
|
||||||
|
value: 0,
|
||||||
|
rangeMin: 0,
|
||||||
|
rangeMax: 1000,
|
||||||
|
currentMonth: {
|
||||||
|
short: this.$moment().format('MMMM'),
|
||||||
|
long: this.$moment().format('DD/MM/YYYY'),
|
||||||
|
},
|
||||||
|
lastMonth: {
|
||||||
|
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||||
|
long: this.$moment().subtract(1, 'month').format('DD/MM/YYYY'),
|
||||||
|
},
|
||||||
|
beforeLastMonth: {
|
||||||
|
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||||
|
long: this.$moment().subtract(2, 'month').format('DD/MM/YYYY'),
|
||||||
|
},
|
||||||
|
submitObj: null,
|
||||||
|
isdisabled: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// Auswählen eines Zeitraumes
|
||||||
|
updateRadioSelected(name, index, openCreation) {
|
||||||
|
// Wenn Mehrfachschöpfung
|
||||||
|
if (this.type === 'massCreation') {
|
||||||
|
// An Creation.vue emitten und radioSelectedMass aktualisieren
|
||||||
|
this.$emit('update-radio-selected', [name, index])
|
||||||
|
}
|
||||||
|
// Wenn Einzelschöpfung
|
||||||
|
if (this.type === 'singleCreation') {
|
||||||
|
this.rangeMin = 0
|
||||||
|
// Der maximale offene Betrag an GDD die für ein User noch geschöpft werden kann
|
||||||
|
this.rangeMax = openCreation
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkFormForUpdate(input) {
|
||||||
|
switch (input) {
|
||||||
|
case 'text':
|
||||||
|
this.text = this.creationUserData.text
|
||||||
|
break
|
||||||
|
case 'range':
|
||||||
|
this.value = this.creationUserData.creationGdd
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// TODO: Toast
|
||||||
|
alert("I don't know such values")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitCreation() {
|
||||||
|
// Formular Prüfen ob ein Zeitraum ausgewählt wurde. Ansonsten abbrechen und Hinweis anzeigen
|
||||||
|
if (this.radioSelected === '') {
|
||||||
|
return alert('Bitte wähle einen Zeitraum!')
|
||||||
|
}
|
||||||
|
// Formular Prüfen ob der GDD Betrag grösser 0 ist. Ansonsten abbrechen und Hinweis anzeigen
|
||||||
|
if (this.value === 0) {
|
||||||
|
return alert('Bitte gib einen GDD Betrag an!')
|
||||||
|
}
|
||||||
|
// Formular Prüfen ob der Text vorhanden ist. Ansonsten abbrechen und Hinweis anzeigen
|
||||||
|
if (this.text === '') {
|
||||||
|
return alert('Bitte gib einen Text ein!')
|
||||||
|
}
|
||||||
|
// Formular Prüfen ob der Text länger als 10 Zeichen hat. Ansonsten abbrechen und Hinweis anzeigen
|
||||||
|
if (this.text.length < 10) {
|
||||||
|
return alert('Bitte gib einen Text ein der länger als 10 Zeichen ist!')
|
||||||
|
}
|
||||||
|
if (this.type === 'massCreation') {
|
||||||
|
// Die anzahl der Mitglieder aus der Mehrfachschöpfung
|
||||||
|
const i = Object.keys(this.itemsMassCreation).length
|
||||||
|
// hinweis das eine Mehrfachschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird)
|
||||||
|
alert('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
|
||||||
|
this.submitObj = [
|
||||||
|
{
|
||||||
|
item: this.itemsMassCreation,
|
||||||
|
datum: this.radioSelected,
|
||||||
|
amount: this.value,
|
||||||
|
text: this.text,
|
||||||
|
moderator: this.$store.state.moderator,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
alert('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
|
||||||
|
|
||||||
|
// $store - offene Schöpfungen hochzählen
|
||||||
|
this.$store.commit('openCreationsPlus', i)
|
||||||
|
|
||||||
|
// lösche alle Mitglieder aus der MehrfachSchöpfungsListe nach dem alle Mehrfachschpfungen zum bestätigen gesendet wurden.
|
||||||
|
this.$emit('remove-all-bookmark')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'singleCreation') {
|
||||||
|
// hinweis das eine einzelne schöpfung ausgeführt wird an (Vorname)
|
||||||
|
alert('SUBMIT CREATION => ' + this.type + ' >> für ' + this.item.firstName + '')
|
||||||
|
// erstellen eines Arrays (submitObj) mit allen Daten
|
||||||
|
this.submitObj = [
|
||||||
|
{
|
||||||
|
item: this.item,
|
||||||
|
datum: this.radioSelected.long,
|
||||||
|
amount: this.value,
|
||||||
|
text: this.text,
|
||||||
|
moderator: this.$store.state.moderator,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.pagetype === 'PageCreationConfirm') {
|
||||||
|
// 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.$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 + '')
|
||||||
|
// $store - offene Schöpfungen hochzählen
|
||||||
|
this.$store.commit('openCreationsPlus', 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// das absendeergebniss im string ansehen
|
||||||
|
alert(JSON.stringify(this.submitObj))
|
||||||
|
// das submitObj zurücksetzen
|
||||||
|
this.submitObj = null
|
||||||
|
// das creation Formular reseten
|
||||||
|
this.$refs.creationForm.reset()
|
||||||
|
// Den geschöpften Wert auf o setzen
|
||||||
|
this.value = 0
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
30
admin/src/components/NavBar.spec.js
Normal file
30
admin/src/components/NavBar.spec.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import NavBar from './NavBar.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
openCreations: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('NavBar', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(NavBar, { mocks, localVue })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV element with the class.component-nabvar', () => {
|
||||||
|
expect(wrapper.find('.component-nabvar').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
58
admin/src/components/NavBar.vue
Normal file
58
admin/src/components/NavBar.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div class="component-nabvar">
|
||||||
|
<b-navbar toggleable="sm" type="dark" variant="success">
|
||||||
|
<b-navbar-brand to="/">Adminbereich</b-navbar-brand>
|
||||||
|
|
||||||
|
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
||||||
|
|
||||||
|
<b-collapse id="nav-collapse" is-nav>
|
||||||
|
<b-navbar-nav>
|
||||||
|
<b-nav-item to="/user">Usersuche |</b-nav-item>
|
||||||
|
<b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item>
|
||||||
|
<b-nav-item
|
||||||
|
v-show="$store.state.openCreations > 0"
|
||||||
|
class="h5 bg-danger"
|
||||||
|
to="/creation-confirm"
|
||||||
|
>
|
||||||
|
| {{ $store.state.openCreations }} offene Schöpfungen
|
||||||
|
</b-nav-item>
|
||||||
|
<b-nav-item @click="wallet">Wallet</b-nav-item>
|
||||||
|
<b-nav-item @click="logout">Logout</b-nav-item>
|
||||||
|
<!-- <b-nav-item v-show="open < 1" to="/creation-confirm">| keine offene Schöpfungen</b-nav-item> -->
|
||||||
|
</b-navbar-nav>
|
||||||
|
</b-collapse>
|
||||||
|
</b-navbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import CONFIG from '../config'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'navbar',
|
||||||
|
methods: {
|
||||||
|
logout() {
|
||||||
|
// TODO
|
||||||
|
// this.$emit('logout')
|
||||||
|
/* this.$apollo
|
||||||
|
.query({
|
||||||
|
query: logout,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.$store.dispatch('logout')
|
||||||
|
this.$router.push('/logout')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$store.dispatch('logout')
|
||||||
|
if (this.$router.currentRoute.path !== '/logout') this.$router.push('/logout')
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
this.$store.dispatch('logout')
|
||||||
|
this.$router.push('/logout')
|
||||||
|
},
|
||||||
|
wallet() {
|
||||||
|
window.location = CONFIG.WALLET_AUTH_URL.replace('$1', this.$store.state.token)
|
||||||
|
this.$store.dispatch('logout') // logout without redirect
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
29
admin/src/components/UserTable.spec.js
Normal file
29
admin/src/components/UserTable.spec.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import UserTable from './UserTable.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('UserTable', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
type: 'Type',
|
||||||
|
itemsUser: [],
|
||||||
|
fieldsTable: [],
|
||||||
|
creation: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(UserTable, { localVue, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV element with the class.component-user-table', () => {
|
||||||
|
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
254
admin/src/components/UserTable.vue
Normal file
254
admin/src/components/UserTable.vue
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
<template>
|
||||||
|
<div class="component-user-table">
|
||||||
|
<div v-show="overlay" id="overlay" class="">
|
||||||
|
<b-jumbotron class="bg-light p-4">
|
||||||
|
<template #header>{{ overlayText.header }}</template>
|
||||||
|
|
||||||
|
<template #lead>
|
||||||
|
{{ overlayText.text1 }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ overlayText.text2 }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<b-button size="lg" variant="danger" class="m-3" @click="overlayCancel">
|
||||||
|
{{ overlayText.button_cancel }}
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
size="lg"
|
||||||
|
variant="success"
|
||||||
|
class="m-3 text-right"
|
||||||
|
@click="overlayOK(overlayBookmarkType, overlayItem)"
|
||||||
|
>
|
||||||
|
{{ overlayText.button_ok }}
|
||||||
|
</b-button>
|
||||||
|
</b-jumbotron>
|
||||||
|
</div>
|
||||||
|
<b-table-lite
|
||||||
|
:items="itemsUser"
|
||||||
|
:fields="fieldsTable"
|
||||||
|
:filter="criteria"
|
||||||
|
caption-top
|
||||||
|
striped
|
||||||
|
hover
|
||||||
|
stacked="md"
|
||||||
|
>
|
||||||
|
<template #cell(edit_creation)="row">
|
||||||
|
<b-button
|
||||||
|
variant="info"
|
||||||
|
size="lg"
|
||||||
|
@click="editCreationUserTable(row, row.item)"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<b-icon v-if="row.detailsShowing" icon="x" aria-label="Help"></b-icon>
|
||||||
|
<b-icon v-else icon="pencil-square" aria-label="Help"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(show_details)="row">
|
||||||
|
<b-button variant="info" size="lg" @click="row.toggleDetails" class="mr-2">
|
||||||
|
<b-icon v-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon>
|
||||||
|
<b-icon v-else icon="eye-fill" aria-label="Help"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #row-details="row">
|
||||||
|
<b-card class="shadow-lg p-3 mb-5 bg-white rounded">
|
||||||
|
<b-row class="mb-2">
|
||||||
|
<b-col></b-col>
|
||||||
|
</b-row>
|
||||||
|
|
||||||
|
<creation-formular
|
||||||
|
type="singleCreation"
|
||||||
|
:pagetype="type"
|
||||||
|
:creation="row.item.creation"
|
||||||
|
:item="row.item"
|
||||||
|
:creationUserData="creationData"
|
||||||
|
@update-creation-data="updateCreationData"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<b-button size="sm" @click="row.toggleDetails">
|
||||||
|
<b-icon
|
||||||
|
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
|
||||||
|
aria-label="Help"
|
||||||
|
></b-icon>
|
||||||
|
Details verbergen von {{ row.item.firstName }} {{ row.item.lastName }}
|
||||||
|
</b-button>
|
||||||
|
</b-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(bookmark)="row">
|
||||||
|
<b-button
|
||||||
|
variant="warning"
|
||||||
|
v-show="type === 'UserListSearch'"
|
||||||
|
size="md"
|
||||||
|
@click="bookmarkPush(row.item)"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<b-icon icon="plus" variant="success"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
<b-button
|
||||||
|
variant="danger"
|
||||||
|
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
|
||||||
|
size="lg"
|
||||||
|
@click="overlayShow('remove', row.item)"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<b-icon icon="x" variant="light"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #cell(confirm)="row">
|
||||||
|
<b-button
|
||||||
|
variant="success"
|
||||||
|
v-show="type === 'PageCreationConfirm'"
|
||||||
|
size="lg"
|
||||||
|
@click="overlayShow('confirm', row.item)"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<b-icon icon="check" scale="2" variant=""></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
</b-table-lite>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import CreationFormular from '../components/CreationFormular.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserTable',
|
||||||
|
props: {
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
itemsUser: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
fieldsTable: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
criteria: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
creation: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
CreationFormular,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
creationData: {},
|
||||||
|
overlay: false,
|
||||||
|
overlayBookmarkType: '',
|
||||||
|
overlayItem: [],
|
||||||
|
overlayText: [
|
||||||
|
{
|
||||||
|
header: '-',
|
||||||
|
text1: '--',
|
||||||
|
text2: '---',
|
||||||
|
button_ok: 'OK',
|
||||||
|
button_cancel: 'Cancel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
overlayShow(bookmarkType, item) {
|
||||||
|
this.overlay = true
|
||||||
|
this.overlayBookmarkType = bookmarkType
|
||||||
|
this.overlayItem = item
|
||||||
|
|
||||||
|
if (bookmarkType === 'remove') {
|
||||||
|
this.overlayText.header = 'Achtung! Schöpfung löschen!'
|
||||||
|
this.overlayText.text1 =
|
||||||
|
'Nach dem Löschen gibt es keine Möglichkeit mehr diesen Datensatz wiederherzustellen. Es wird aber der gesamte Vorgang in der Logdatei als Übersicht gespeichert.'
|
||||||
|
this.overlayText.text2 = 'Willst du die vorgespeicherte Schöpfung wirklich löschen? '
|
||||||
|
this.overlayText.button_ok = 'Ja, Schöpfung löschen!'
|
||||||
|
this.overlayText.button_cancel = 'Nein, nicht löschen.'
|
||||||
|
}
|
||||||
|
if (bookmarkType === 'confirm') {
|
||||||
|
this.overlayText.header = 'Schöpfung bestätigen!'
|
||||||
|
this.overlayText.text1 =
|
||||||
|
'Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.'
|
||||||
|
this.overlayText.text2 =
|
||||||
|
'Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?'
|
||||||
|
this.overlayText.button_ok = 'Ja, Schöpfung speichern und bestätigen!'
|
||||||
|
this.overlayText.button_cancel = 'Nein, nicht speichern.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
overlayOK(bookmarkType, item) {
|
||||||
|
if (bookmarkType === 'remove') {
|
||||||
|
this.bookmarkRemove(item)
|
||||||
|
}
|
||||||
|
if (bookmarkType === 'confirm') {
|
||||||
|
this.bookmarkConfirm(item)
|
||||||
|
}
|
||||||
|
this.overlay = false
|
||||||
|
},
|
||||||
|
overlayCancel() {
|
||||||
|
this.overlay = false
|
||||||
|
},
|
||||||
|
bookmarkPush(item) {
|
||||||
|
this.$emit('update-item', item, 'push')
|
||||||
|
},
|
||||||
|
bookmarkRemove(item) {
|
||||||
|
if (this.type === 'UserListMassCreation') {
|
||||||
|
this.$emit('update-item', item, 'remove')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type === 'PageCreationConfirm') {
|
||||||
|
this.$emit('remove-confirm-result', item, 'remove')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bookmarkConfirm(item) {
|
||||||
|
alert('die schöpfung bestätigen und abschließen')
|
||||||
|
alert(JSON.stringify(item))
|
||||||
|
this.$emit('remove-confirm-result', item, 'remove')
|
||||||
|
},
|
||||||
|
editCreationUserTable(row, rowItem) {
|
||||||
|
alert('editCreationUserTable')
|
||||||
|
if (!row.detailsShowing) {
|
||||||
|
alert('offen edit loslegen')
|
||||||
|
// this.item = rowItem
|
||||||
|
this.creationData = rowItem
|
||||||
|
// alert(this.creationData)
|
||||||
|
}
|
||||||
|
row.toggleDetails()
|
||||||
|
},
|
||||||
|
updateCreationData(data) {
|
||||||
|
this.creationData = {
|
||||||
|
...data,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
#overlay {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
padding-left: 5%;
|
||||||
|
background-color: rgba(12, 11, 11, 0.781);
|
||||||
|
z-index: 1000000;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -17,8 +17,13 @@ const environment = {
|
|||||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = {
|
const endpoints = {
|
||||||
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
|
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 = {}
|
const options = {}
|
||||||
@ -26,8 +31,9 @@ const options = {}
|
|||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
...version,
|
...version,
|
||||||
...environment,
|
...environment,
|
||||||
...server,
|
...endpoints,
|
||||||
...options,
|
...options,
|
||||||
|
...debug,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CONFIG
|
export default CONFIG
|
||||||
|
|||||||
12
admin/src/graphql/searchUsers.js
Normal file
12
admin/src/graphql/searchUsers.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const searchUsers = gql`
|
||||||
|
query ($searchText: String!) {
|
||||||
|
searchUsers(searchText: $searchText) {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
email
|
||||||
|
creation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
30
admin/src/i18n.test.js
Normal file
30
admin/src/i18n.test.js
Normal file
@ -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(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
19
admin/src/layouts/defaultLayout.vue
Normal file
19
admin/src/layouts/defaultLayout.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nav-bar class="wrapper-nav" />
|
||||||
|
<router-view class="wrapper p-3"></router-view>
|
||||||
|
<content-footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NavBar from '@/components/NavBar.vue'
|
||||||
|
import ContentFooter from '@/components/ContentFooter.vue'
|
||||||
|
export default {
|
||||||
|
name: 'defaultLayout',
|
||||||
|
components: {
|
||||||
|
NavBar,
|
||||||
|
ContentFooter,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -16,12 +16,17 @@ import VueApollo from 'vue-apollo'
|
|||||||
|
|
||||||
import CONFIG from './config'
|
import CONFIG from './config'
|
||||||
|
|
||||||
import { BootstrapVue } from 'bootstrap-vue'
|
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css'
|
||||||
|
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||||
|
|
||||||
|
import moment from 'vue-moment'
|
||||||
|
|
||||||
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
|
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
|
||||||
|
|
||||||
const authLink = new ApolloLink((operation, forward) => {
|
const authLink = new ApolloLink((operation, forward) => {
|
||||||
const token = store.state.token
|
const token = store.state.token
|
||||||
|
|
||||||
operation.setContext({
|
operation.setContext({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||||
@ -52,9 +57,16 @@ const apolloProvider = new VueApollo({
|
|||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
|
Vue.use(IconsPlugin)
|
||||||
|
|
||||||
|
Vue.use(moment)
|
||||||
|
|
||||||
|
Vue.use(VueApollo)
|
||||||
|
|
||||||
addNavigationGuards(router, store)
|
addNavigationGuards(router, store)
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
|
moment,
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
i18n,
|
i18n,
|
||||||
|
|||||||
@ -5,10 +5,13 @@ import CONFIG from './config'
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import VueI18n from 'vue-i18n'
|
import VueI18n from 'vue-i18n'
|
||||||
|
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||||
|
import moment from 'vue-moment'
|
||||||
|
|
||||||
jest.mock('vue')
|
jest.mock('vue')
|
||||||
jest.mock('vuex')
|
jest.mock('vuex')
|
||||||
jest.mock('vue-i18n')
|
jest.mock('vue-i18n')
|
||||||
|
jest.mock('vue-moment')
|
||||||
|
|
||||||
const storeMock = jest.fn()
|
const storeMock = jest.fn()
|
||||||
Vuex.Store = storeMock
|
Vuex.Store = storeMock
|
||||||
@ -25,6 +28,16 @@ jest.mock('apollo-boost', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
jest.mock('bootstrap-vue', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
BootstrapVue: jest.fn(),
|
||||||
|
IconsPlugin: jest.fn(() => {
|
||||||
|
return { concat: jest.fn() }
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe('main', () => {
|
describe('main', () => {
|
||||||
it('calls the HttpLink', () => {
|
it('calls the HttpLink', () => {
|
||||||
expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI })
|
expect(HttpLink).toBeCalledWith({ uri: CONFIG.GRAPHQL_URI })
|
||||||
@ -50,6 +63,18 @@ describe('main', () => {
|
|||||||
expect(VueI18n).toBeCalled()
|
expect(VueI18n).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.skip('calls BootstrapVue', () => {
|
||||||
|
expect(BootstrapVue).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls IconsPlugin', () => {
|
||||||
|
expect(IconsPlugin).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls Moment', () => {
|
||||||
|
expect(moment).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it.skip('creates a store', () => {
|
it.skip('creates a store', () => {
|
||||||
expect(storeMock).toBeCalled()
|
expect(storeMock).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|||||||
59
admin/src/pages/Creation.spec.js
Normal file
59
admin/src/pages/Creation.spec.js
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
143
admin/src/pages/Creation.vue
Normal file
143
admin/src/pages/Creation.vue
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="creation">
|
||||||
|
<b-row>
|
||||||
|
<b-col cols="12" lg="5">
|
||||||
|
<label>Usersuche</label>
|
||||||
|
<b-input
|
||||||
|
type="text"
|
||||||
|
v-model="criteria"
|
||||||
|
class="shadow p-3 mb-5 bg-white rounded"
|
||||||
|
placeholder="User suche"
|
||||||
|
></b-input>
|
||||||
|
<user-table
|
||||||
|
v-if="itemsList.length > 0"
|
||||||
|
type="UserListSearch"
|
||||||
|
:itemsUser="itemsList"
|
||||||
|
:fieldsTable="Searchfields"
|
||||||
|
:criteria="criteria"
|
||||||
|
:creation="creation"
|
||||||
|
@update-item="updateItem"
|
||||||
|
/>
|
||||||
|
</b-col>
|
||||||
|
<b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info">
|
||||||
|
<user-table
|
||||||
|
v-if="massCreation.length > 0"
|
||||||
|
class="shadow p-3 mb-5 bg-white rounded"
|
||||||
|
type="UserListMassCreation"
|
||||||
|
:itemsUser="massCreation"
|
||||||
|
:fieldsTable="fields"
|
||||||
|
:criteria="null"
|
||||||
|
:creation="creation"
|
||||||
|
@update-item="updateItem"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<creation-formular
|
||||||
|
v-if="massCreation.length > 0"
|
||||||
|
type="massCreation"
|
||||||
|
:creation="creation"
|
||||||
|
:itemsMassCreation="massCreation"
|
||||||
|
@update-radio-selected="updateRadioSelected"
|
||||||
|
@remove-all-bookmark="removeAllBookmark"
|
||||||
|
/>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import CreationFormular from '../components/CreationFormular.vue'
|
||||||
|
import UserTable from '../components/UserTable.vue'
|
||||||
|
import { searchUsers } from '../graphql/searchUsers'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Creation',
|
||||||
|
components: {
|
||||||
|
CreationFormular,
|
||||||
|
UserTable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showArrays: false,
|
||||||
|
Searchfields: [
|
||||||
|
{ key: 'bookmark', label: 'merken' },
|
||||||
|
{ key: 'firstName', label: 'Firstname' },
|
||||||
|
{ key: 'lastName', label: 'Lastname' },
|
||||||
|
{ key: 'creation', label: 'Creation' },
|
||||||
|
{ key: 'email', label: 'Email' },
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: 'email', label: 'Email' },
|
||||||
|
{ key: 'firstName', label: 'Firstname' },
|
||||||
|
{ key: 'lastName', label: 'Lastname' },
|
||||||
|
{ key: 'creation', label: 'Creation' },
|
||||||
|
{ key: 'bookmark', label: 'löschen' },
|
||||||
|
],
|
||||||
|
itemsList: [],
|
||||||
|
massCreation: [],
|
||||||
|
radioSelectedMass: '',
|
||||||
|
criteria: '',
|
||||||
|
creation: [null, null, null],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.getUsers()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async getUsers() {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: searchUsers,
|
||||||
|
variables: {
|
||||||
|
searchText: this.criteria,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.itemsList = result.data.searchUsers.map((user) => {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
showDetails: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toasted.error(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateItem(e, event) {
|
||||||
|
let index = 0
|
||||||
|
let findArr = {}
|
||||||
|
|
||||||
|
switch (event) {
|
||||||
|
case 'push':
|
||||||
|
findArr = this.itemsList.find((arr) => arr.id === e.id)
|
||||||
|
index = this.itemsList.indexOf(findArr)
|
||||||
|
this.itemsList.splice(index, 1)
|
||||||
|
this.massCreation.push(e)
|
||||||
|
break
|
||||||
|
case 'remove':
|
||||||
|
findArr = this.massCreation.find((arr) => arr.id === e.id)
|
||||||
|
index = this.massCreation.indexOf(findArr)
|
||||||
|
this.massCreation.splice(index, 1)
|
||||||
|
this.itemsList.push(e)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(event)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRadioSelected(obj) {
|
||||||
|
this.radioSelectedMass = obj[0]
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAllBookmark() {
|
||||||
|
alert('remove all bookmarks')
|
||||||
|
const index = 0
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
for (i; i < this.massCreation.length; i++) {
|
||||||
|
this.itemsList.push(this.massCreation[i])
|
||||||
|
}
|
||||||
|
this.massCreation.splice(index, this.massCreation.length)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
admin/src/pages/CreationConfirm.spec.js
Normal file
53
admin/src/pages/CreationConfirm.spec.js
Normal file
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
149
admin/src/pages/CreationConfirm.vue
Normal file
149
admin/src/pages/CreationConfirm.vue
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<template>
|
||||||
|
<div class="creation-confirm">
|
||||||
|
<small class="bg-danger text-light p-1">
|
||||||
|
Die anzahl der offene Schöpfungen stimmen nicht! Diese wird bei absenden im $store
|
||||||
|
hochgezählt. Die Liste die hier angezeigt wird ist SIMULIERT!
|
||||||
|
</small>
|
||||||
|
<user-table
|
||||||
|
class="mt-4"
|
||||||
|
type="PageCreationConfirm"
|
||||||
|
:itemsUser="confirmResult"
|
||||||
|
:fieldsTable="fields"
|
||||||
|
@remove-confirm-result="removeConfirmResult"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import UserTable from '../components/UserTable.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CreationConfirm',
|
||||||
|
components: {
|
||||||
|
UserTable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showArrays: false,
|
||||||
|
fields: [
|
||||||
|
{ key: 'bookmark', label: 'löschen' },
|
||||||
|
{ key: 'email', label: 'Email' },
|
||||||
|
{ key: 'firstName', label: 'Vorname' },
|
||||||
|
{ key: 'lastName', label: 'Nachname' },
|
||||||
|
{
|
||||||
|
key: 'creation_gdd',
|
||||||
|
label: 'Schöpfung',
|
||||||
|
formatter: (value) => {
|
||||||
|
return value + ' GDD'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'text', label: 'Text' },
|
||||||
|
{
|
||||||
|
key: 'creation_date',
|
||||||
|
label: 'Datum',
|
||||||
|
formatter: (value) => {
|
||||||
|
return value.long
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'creation_moderator', label: 'Moderator' },
|
||||||
|
{ key: 'edit_creation', label: 'ändern' },
|
||||||
|
{ key: 'confirm', label: 'speichern' },
|
||||||
|
],
|
||||||
|
confirmResult: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
email: 'dickerson@web.de',
|
||||||
|
firstName: 'Dickerson',
|
||||||
|
lastName: 'Macdonald',
|
||||||
|
creation: '[450,200,700]',
|
||||||
|
creation_gdd: '1000',
|
||||||
|
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam ',
|
||||||
|
|
||||||
|
creation_date: {
|
||||||
|
short: 'November',
|
||||||
|
long: '22/11/2021',
|
||||||
|
},
|
||||||
|
creation_moderator: 'Manuela Gast',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
email: 'larsen@woob.de',
|
||||||
|
firstName: 'Larsen',
|
||||||
|
lastName: 'Shaw',
|
||||||
|
creation: '[300,200,1000]',
|
||||||
|
creation_gdd: '1000',
|
||||||
|
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam ',
|
||||||
|
|
||||||
|
creation_date: {
|
||||||
|
short: 'November',
|
||||||
|
long: '03/11/2021',
|
||||||
|
},
|
||||||
|
creation_moderator: 'Manuela Gast',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
email: 'geneva@tete.de',
|
||||||
|
firstName: 'Geneva',
|
||||||
|
lastName: 'Wilson',
|
||||||
|
creation: '[350,200,900]',
|
||||||
|
creation_gdd: '1000',
|
||||||
|
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam',
|
||||||
|
creation_date: {
|
||||||
|
short: 'September',
|
||||||
|
long: '27/09/2021',
|
||||||
|
},
|
||||||
|
creation_moderator: 'Manuela Gast',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
email: 'viewrter@asdfvb.com',
|
||||||
|
firstName: 'Soledare',
|
||||||
|
lastName: 'Takker',
|
||||||
|
creation: '[100,400,800]',
|
||||||
|
creation_gdd: '500',
|
||||||
|
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo ',
|
||||||
|
creation_date: {
|
||||||
|
short: 'Oktober',
|
||||||
|
long: '12/10/2021',
|
||||||
|
},
|
||||||
|
creation_moderator: 'Evelyn Roller',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
email: 'dickerson@web.de',
|
||||||
|
firstName: 'Dickerson',
|
||||||
|
lastName: 'Macdonald',
|
||||||
|
creation: '[100,400,800]',
|
||||||
|
creation_gdd: '200',
|
||||||
|
text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At',
|
||||||
|
creation_date: {
|
||||||
|
short: 'September',
|
||||||
|
long: '05/09/2021',
|
||||||
|
},
|
||||||
|
creation_moderator: 'Manuela Gast',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
removeConfirmResult(e, event) {
|
||||||
|
if (event === 'remove') {
|
||||||
|
let index = 0
|
||||||
|
let findArr = {}
|
||||||
|
|
||||||
|
findArr = this.confirmResult.find((arr) => arr.id === e.id)
|
||||||
|
|
||||||
|
index = this.confirmResult.indexOf(findArr)
|
||||||
|
|
||||||
|
this.confirmResult.splice(index, 1)
|
||||||
|
|
||||||
|
this.$store.commit('openCreationsMinus', 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$store.commit('resetOpenCreations')
|
||||||
|
this.$store.commit('openCreationsPlus', Object.keys(this.confirmResult).length)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
82
admin/src/pages/Overview.vue
Normal file
82
admin/src/pages/Overview.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<b-card
|
||||||
|
v-show="$store.state.openCreations > 0"
|
||||||
|
border-variant="primary"
|
||||||
|
header="offene Schöpfungen"
|
||||||
|
header-bg-variant="danger"
|
||||||
|
header-text-variant="white"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<b-card-text>
|
||||||
|
<b-link to="creation-confirm">
|
||||||
|
<h1>{{ $store.state.openCreations }}</h1>
|
||||||
|
</b-link>
|
||||||
|
</b-card-text>
|
||||||
|
</b-card>
|
||||||
|
<b-card
|
||||||
|
v-show="$store.state.openCreations < 1"
|
||||||
|
border-variant="success"
|
||||||
|
header="keine offene Schöpfungen"
|
||||||
|
header-bg-variant="success"
|
||||||
|
header-text-variant="white"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<b-card-text>
|
||||||
|
<b-link to="creation-confirm">
|
||||||
|
<h1>{{ $store.state.openCreations }}</h1>
|
||||||
|
</b-link>
|
||||||
|
</b-card-text>
|
||||||
|
</b-card>
|
||||||
|
<br />
|
||||||
|
<b-row>
|
||||||
|
<b-col>
|
||||||
|
<b-card border-variant="info" header="offene Registrierung" align="center">
|
||||||
|
<b-card-text>Unbestätigte E-mail Registrierung</b-card-text>
|
||||||
|
</b-card>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-card border-variant="info" header="geschöpfte Stunden" align="center">
|
||||||
|
<b-card-text>Wievile Stunden können noch von Mitgliedern geschöpft werden?</b-card-text>
|
||||||
|
</b-card>
|
||||||
|
</b-col>
|
||||||
|
<b-col>
|
||||||
|
<b-card border-variant="info" header="Gemeinschafts Konto" align="center">
|
||||||
|
<b-card-text>
|
||||||
|
Für jedes Mitglied kann für das Gemeinschaftskonto geschöpft werden. Pro Monat 1000 x
|
||||||
|
Mitglieder
|
||||||
|
</b-card-text>
|
||||||
|
</b-card>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
<hr />
|
||||||
|
<br />
|
||||||
|
<b-list-group>
|
||||||
|
<b-list-group-item class="bg-secondary text-light" href="user">
|
||||||
|
zur Usersuche
|
||||||
|
</b-list-group-item>
|
||||||
|
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||||
|
Mitglieder
|
||||||
|
<b-badge class="bg-success" pill>14</b-badge>
|
||||||
|
</b-list-group-item>
|
||||||
|
|
||||||
|
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||||
|
aktive Mitglieder
|
||||||
|
<b-badge class="bg-primary" pill>12</b-badge>
|
||||||
|
</b-list-group-item>
|
||||||
|
|
||||||
|
<b-list-group-item class="d-flex justify-content-between align-items-center">
|
||||||
|
nicht bestätigte Mitglieder
|
||||||
|
<b-badge class="bg-warning text-dark" pill>2</b-badge>
|
||||||
|
</b-list-group-item>
|
||||||
|
</b-list-group>
|
||||||
|
<b-button @click="$store.commit('resetOpenCreations')">
|
||||||
|
lösche alle offenen Test Schöpfungen
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'overview',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
59
admin/src/pages/UserSearch.spec.js
Normal file
59
admin/src/pages/UserSearch.spec.js
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
70
admin/src/pages/UserSearch.vue
Normal file
70
admin/src/pages/UserSearch.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-search">
|
||||||
|
<label>Usersuche</label>
|
||||||
|
<b-input
|
||||||
|
type="text"
|
||||||
|
v-model="criteria"
|
||||||
|
class="shadow p-3 mb-5 bg-white rounded"
|
||||||
|
placeholder="User suche"
|
||||||
|
@input="getUsers"
|
||||||
|
></b-input>
|
||||||
|
<user-table
|
||||||
|
type="PageUserSearch"
|
||||||
|
:itemsUser="searchResult"
|
||||||
|
:fieldsTable="fields"
|
||||||
|
:criteria="criteria"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import UserTable from '../components/UserTable.vue'
|
||||||
|
import { searchUsers } from '../graphql/searchUsers'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'UserSearch',
|
||||||
|
components: {
|
||||||
|
UserTable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showArrays: false,
|
||||||
|
fields: [
|
||||||
|
{ key: 'email', label: 'Email' },
|
||||||
|
{ key: 'firstName', label: 'Firstname' },
|
||||||
|
{ key: 'lastName', label: 'Lastname' },
|
||||||
|
{ key: 'creation', label: 'Creation' },
|
||||||
|
{ key: 'show_details', label: 'Details' },
|
||||||
|
],
|
||||||
|
searchResult: [],
|
||||||
|
massCreation: [],
|
||||||
|
criteria: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getUsers() {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: searchUsers,
|
||||||
|
variables: {
|
||||||
|
searchText: this.criteria,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.searchResult = result.data.searchUsers.map((user) => {
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
// showDetails: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.$toasted.error(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getUsers()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,7 +1,25 @@
|
|||||||
|
import CONFIG from '../config'
|
||||||
|
|
||||||
const addNavigationGuards = (router, store) => {
|
const addNavigationGuards = (router, store) => {
|
||||||
|
// store token on `authenticate`
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// handle authentication
|
if (to.path === '/authenticate' && to.query && to.query.token) {
|
||||||
if (to.meta.requiresAuth && !store.state.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' })
|
next({ path: '/not-found' })
|
||||||
} else {
|
} else {
|
||||||
next()
|
next()
|
||||||
|
|||||||
64
admin/src/router/guards.test.js
Normal file
64
admin/src/router/guards.test.js
Normal file
@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
92
admin/src/router/router.test.js
Normal file
92
admin/src/router/router.test.js
Normal file
@ -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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,15 +1,32 @@
|
|||||||
import NotFound from '@/components/NotFoundPage.vue'
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/authenticate',
|
||||||
/*
|
},
|
||||||
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('@/pages/UserSearch.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/creation',
|
||||||
|
component: () => import('@/pages/Creation.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/creation-confirm',
|
||||||
|
component: () => import('@/pages/CreationConfirm.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
component: () => import('@/components/NotFoundPage.vue'),
|
||||||
},
|
},
|
||||||
{ path: '*', component: NotFound },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export default routes
|
export default routes
|
||||||
|
|||||||
@ -1,19 +1,46 @@
|
|||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import createPersistedState from 'vuex-persistedstate'
|
||||||
|
import CONFIG from '../config'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
openCreationsPlus: (state, i) => {
|
||||||
|
state.openCreations += i
|
||||||
|
},
|
||||||
|
openCreationsMinus: (state, i) => {
|
||||||
|
state.openCreations -= i
|
||||||
|
},
|
||||||
|
resetOpenCreations: (state) => {
|
||||||
|
state.openCreations = 0
|
||||||
|
},
|
||||||
token: (state, token) => {
|
token: (state, token) => {
|
||||||
state.token = token
|
state.token = token
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
export const actions = {
|
||||||
mutations,
|
logout: ({ commit, state }) => {
|
||||||
state: {
|
commit('token', null)
|
||||||
token: 'some-token',
|
window.localStorage.clear()
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Vuex.Store({
|
||||||
|
plugins: [
|
||||||
|
createPersistedState({
|
||||||
|
storage: window.localStorage,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
state: {
|
||||||
|
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
|
||||||
|
moderator: 'Dertest Moderator',
|
||||||
|
openCreations: 0,
|
||||||
|
},
|
||||||
|
// Syncronous mutation of the state
|
||||||
|
mutations,
|
||||||
|
actions,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default store
|
export default store
|
||||||
|
|||||||
@ -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('Vuex store', () => {
|
||||||
describe('mutations', () => {
|
describe('mutations', () => {
|
||||||
@ -11,5 +16,68 @@ describe('Vuex store', () => {
|
|||||||
expect(state.token).toEqual('1234')
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { createLocalVue } from '@vue/test-utils'
|
import { createLocalVue } from '@vue/test-utils'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { BootstrapVue } from 'bootstrap-vue'
|
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||||
|
|
||||||
// without this async calls are not working
|
// without this async calls are not working
|
||||||
import 'regenerator-runtime'
|
import 'regenerator-runtime'
|
||||||
@ -8,6 +8,7 @@ import 'regenerator-runtime'
|
|||||||
global.localVue = createLocalVue()
|
global.localVue = createLocalVue()
|
||||||
|
|
||||||
global.localVue.use(BootstrapVue)
|
global.localVue.use(BootstrapVue)
|
||||||
|
global.localVue.use(IconsPlugin)
|
||||||
|
|
||||||
// throw errors for vue warnings to force the programmers to take care about warnings
|
// throw errors for vue warnings to force the programmers to take care about warnings
|
||||||
Vue.config.warnHandler = (w) => {
|
Vue.config.warnHandler = (w) => {
|
||||||
|
|||||||
@ -6424,6 +6424,11 @@ har-validator@~5.1.3:
|
|||||||
ajv "^6.12.3"
|
ajv "^6.12.3"
|
||||||
har-schema "^2.0.0"
|
har-schema "^2.0.0"
|
||||||
|
|
||||||
|
harmony-reflect@^1.4.6:
|
||||||
|
version "1.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/harmony-reflect/-/harmony-reflect-1.6.2.tgz#31ecbd32e648a34d030d86adb67d4d47547fe710"
|
||||||
|
integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==
|
||||||
|
|
||||||
has-ansi@^2.0.0:
|
has-ansi@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||||
@ -6776,6 +6781,13 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.14"
|
postcss "^7.0.14"
|
||||||
|
|
||||||
|
identity-obj-proxy@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
|
||||||
|
integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=
|
||||||
|
dependencies:
|
||||||
|
harmony-reflect "^1.4.6"
|
||||||
|
|
||||||
ieee754@^1.1.4:
|
ieee754@^1.1.4:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||||
@ -9020,6 +9032,11 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.5"
|
minimist "^1.2.5"
|
||||||
|
|
||||||
|
moment@^2.19.2, moment@^2.29.1:
|
||||||
|
version "2.29.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||||
|
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||||
|
|
||||||
move-concurrently@^1.0.1:
|
move-concurrently@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||||
@ -11163,6 +11180,11 @@ shellwords@^0.1.1:
|
|||||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||||
|
|
||||||
|
shvl@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/shvl/-/shvl-2.0.3.tgz#eb4bd37644f5684bba1fc52c3010c96fb5e6afd1"
|
||||||
|
integrity sha512-V7C6S9Hlol6SzOJPnQ7qzOVEWUQImt3BNmmzh40wObhla3XOYMe4gGiYzLrJd5TFa+cI2f9LKIRJTTKZSTbWgw==
|
||||||
|
|
||||||
side-channel@^1.0.4:
|
side-channel@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||||
@ -12469,6 +12491,13 @@ vue-loader@^15.9.2:
|
|||||||
vue-hot-reload-api "^2.3.0"
|
vue-hot-reload-api "^2.3.0"
|
||||||
vue-style-loader "^4.1.0"
|
vue-style-loader "^4.1.0"
|
||||||
|
|
||||||
|
vue-moment@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vue-moment/-/vue-moment-4.1.0.tgz#092a8ff723a96c6f85a0a8e23ad30f0bf320f3b0"
|
||||||
|
integrity sha512-Gzisqpg82ItlrUyiD9d0Kfru+JorW2o4mQOH06lEDZNgxci0tv/fua1Hl0bo4DozDV2JK1r52Atn/8QVCu8qQw==
|
||||||
|
dependencies:
|
||||||
|
moment "^2.19.2"
|
||||||
|
|
||||||
vue-router@^3.5.3:
|
vue-router@^3.5.3:
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.3.tgz#041048053e336829d05dafacf6a8fb669a2e7999"
|
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.3.tgz#041048053e336829d05dafacf6a8fb669a2e7999"
|
||||||
@ -12500,6 +12529,14 @@ vue@^2.6.11:
|
|||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||||
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
|
integrity sha512-x2284lgYvjOMj3Za7kqzRcUSxBboHqtgRE2zlos1qWaOye5yUmHn42LB1250NJBLRwEcdrB0JRwyPTEPhfQjiQ==
|
||||||
|
|
||||||
|
vuex-persistedstate@^4.1.0:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/vuex-persistedstate/-/vuex-persistedstate-4.1.0.tgz#127165f85f5b4534fb3170a5d3a8be9811bd2a53"
|
||||||
|
integrity sha512-3SkEj4NqwM69ikJdFVw6gObeB0NHyspRYMYkR/EbhR0hbvAKyR5gksVhtAfY1UYuWUOCCA0QNGwv9pOwdj+XUQ==
|
||||||
|
dependencies:
|
||||||
|
deepmerge "^4.2.2"
|
||||||
|
shvl "^2.0.3"
|
||||||
|
|
||||||
vuex@^3.6.2:
|
vuex@^3.6.2:
|
||||||
version "3.6.2"
|
version "3.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
|
||||||
|
|||||||
@ -28,4 +28,6 @@ COMMUNITY_URL=
|
|||||||
COMMUNITY_REGISTER_URL=
|
COMMUNITY_REGISTER_URL=
|
||||||
COMMUNITY_DESCRIPTION=
|
COMMUNITY_DESCRIPTION=
|
||||||
LOGIN_APP_SECRET=21ffbbc616fe
|
LOGIN_APP_SECRET=21ffbbc616fe
|
||||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||||
|
|
||||||
|
WEBHOOK_ELOPAGE_SECRET=secret
|
||||||
@ -20,6 +20,7 @@
|
|||||||
"apollo-server-express": "^2.25.2",
|
"apollo-server-express": "^2.25.2",
|
||||||
"apollo-server-testing": "^2.25.2",
|
"apollo-server-testing": "^2.25.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
|
|||||||
@ -54,9 +54,21 @@ const email = {
|
|||||||
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1',
|
EMAIL_LINK_SETPASSWORD: process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/reset/$1',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const webhook = {
|
||||||
|
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
||||||
|
}
|
||||||
|
|
||||||
// This is needed by graphql-directive-auth
|
// This is needed by graphql-directive-auth
|
||||||
process.env.APP_SECRET = server.JWT_SECRET
|
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
|
export default CONFIG
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export default class CreateUserArgs {
|
|||||||
lastName: string
|
lastName: string
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
language: string
|
language?: string // Will default to DEFAULT_LANGUAGE
|
||||||
|
|
||||||
@Field(() => Int, { nullable: true })
|
@Field(() => Int, { nullable: true })
|
||||||
publisherId: number
|
publisherId: number
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export class User {
|
|||||||
this.pubkey = json.public_hex
|
this.pubkey = json.public_hex
|
||||||
this.language = json.language
|
this.language = json.language
|
||||||
this.publisherId = json.publisher_id
|
this.publisherId = json.publisher_id
|
||||||
|
this.isAdmin = json.isAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ export class User {
|
|||||||
@Field(() => number)
|
@Field(() => number)
|
||||||
created: number
|
created: number
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() =>>> Boolean)
|
||||||
emailChecked: boolean
|
emailChecked: boolean
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
@ -71,6 +72,9 @@ export class User {
|
|||||||
@Field(() => Int, { nullable: true })
|
@Field(() => Int, { nullable: true })
|
||||||
publisherId?: number
|
publisherId?: number
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
isAdmin: boolean
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
coinanimation: boolean
|
coinanimation: boolean
|
||||||
|
|
||||||
|
|||||||
16
backend/src/graphql/model/UserAdmin.ts
Normal file
16
backend/src/graphql/model/UserAdmin.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class UserAdmin {
|
||||||
|
@Field(() => String)
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
lastName: string
|
||||||
|
|
||||||
|
@Field(() => [Number])
|
||||||
|
creation: number[]
|
||||||
|
}
|
||||||
26
backend/src/graphql/resolver/AdminResolver.ts
Normal file
26
backend/src/graphql/resolver/AdminResolver.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Resolver, Query, Arg } from 'type-graphql'
|
||||||
|
import { getCustomRepository } from 'typeorm'
|
||||||
|
import { UserAdmin } from '../model/UserAdmin'
|
||||||
|
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class AdminResolver {
|
||||||
|
@Query(() => [UserAdmin])
|
||||||
|
async searchUsers(@Arg('searchText') searchText: string): Promise<UserAdmin[]> {
|
||||||
|
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||||
|
const loginUsers = await loginUserRepository.findBySearchCriteria(searchText)
|
||||||
|
const users = loginUsers.map((loginUser) => {
|
||||||
|
const user = new UserAdmin()
|
||||||
|
user.firstName = loginUser.firstName
|
||||||
|
user.lastName = loginUser.lastName
|
||||||
|
user.email = loginUser.email
|
||||||
|
user.creation = [
|
||||||
|
(Math.floor(Math.random() * 50) + 1) * 20,
|
||||||
|
(Math.floor(Math.random() * 50) + 1) * 20,
|
||||||
|
(Math.floor(Math.random() * 50) + 1) * 20,
|
||||||
|
]
|
||||||
|
return user
|
||||||
|
})
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -148,6 +148,69 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
|||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
|
/*
|
||||||
|
@Authorized()
|
||||||
|
@Query(() => User)
|
||||||
|
async verifyLogin(@Ctx() context: any): Promise<User> {
|
||||||
|
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<User> {
|
||||||
|
// 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)
|
@Query(() => User)
|
||||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||||
async login(
|
async login(
|
||||||
@ -229,6 +292,7 @@ export class UserResolver {
|
|||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
})
|
})
|
||||||
user.coinanimation = coinanimation
|
user.coinanimation = coinanimation
|
||||||
|
user.isAdmin = true // TODO implement
|
||||||
|
|
||||||
context.setHeaders.push({
|
context.setHeaders.push({
|
||||||
key: 'token',
|
key: 'token',
|
||||||
@ -257,7 +321,7 @@ export class UserResolver {
|
|||||||
// default int publisher_id = 0;
|
// default int publisher_id = 0;
|
||||||
|
|
||||||
// Validate Language (no throw)
|
// Validate Language (no throw)
|
||||||
if (!isLanguage(language)) {
|
if (!language || !isLanguage(language)) {
|
||||||
language = DEFAULT_LANGUAGE
|
language = DEFAULT_LANGUAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'module-alias/register'
|
|||||||
|
|
||||||
import { ApolloServer } from 'apollo-server-express'
|
import { ApolloServer } from 'apollo-server-express'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import bodyParser from 'body-parser'
|
||||||
|
|
||||||
// database
|
// database
|
||||||
import connection from '../typeorm/connection'
|
import connection from '../typeorm/connection'
|
||||||
@ -22,6 +23,9 @@ import CONFIG from '../config'
|
|||||||
// graphql
|
// graphql
|
||||||
import schema from '../graphql/schema'
|
import schema from '../graphql/schema'
|
||||||
|
|
||||||
|
// webhooks
|
||||||
|
import { elopageWebhook } from '../webhook/elopage'
|
||||||
|
|
||||||
// TODO implement
|
// TODO implement
|
||||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||||
|
|
||||||
@ -50,6 +54,12 @@ const createServer = async (context: any = serverContext): Promise<any> => {
|
|||||||
// cors
|
// cors
|
||||||
app.use(cors)
|
app.use(cors)
|
||||||
|
|
||||||
|
// bodyparser
|
||||||
|
app.use(bodyParser.json())
|
||||||
|
|
||||||
|
// Elopage Webhook
|
||||||
|
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||||
|
|
||||||
// Apollo Server
|
// Apollo Server
|
||||||
const apollo = new ApolloServer({
|
const apollo = new ApolloServer({
|
||||||
schema: await schema(),
|
schema: await schema(),
|
||||||
|
|||||||
@ -8,4 +8,17 @@ export class LoginUserRepository extends Repository<LoginUser> {
|
|||||||
.where('loginUser.email = :email', { email })
|
.where('loginUser.email = :email', { email })
|
||||||
.getOneOrFail()
|
.getOneOrFail()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findBySearchCriteria(searchCriteria: string): Promise<LoginUser[]> {
|
||||||
|
return await this.createQueryBuilder('user')
|
||||||
|
.where(
|
||||||
|
'user.firstName like :name or user.lastName like :lastName or user.email like :email',
|
||||||
|
{
|
||||||
|
name: `%${searchCriteria}%`,
|
||||||
|
lastName: `%${searchCriteria}%`,
|
||||||
|
email: `%${searchCriteria}%`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.getMany()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
154
backend/src/webhook/elopage.ts
Normal file
154
backend/src/webhook/elopage.ts
Normal file
File diff suppressed because one or more lines are too long
@ -1552,7 +1552,7 @@ binary-extensions@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
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"
|
version "1.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||||
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
GRAPHQL_URI=http://localhost:4000/graphql
|
GRAPHQL_URI=http://localhost:4000/graphql
|
||||||
DEFAULT_PUBLISHER_ID=2896
|
DEFAULT_PUBLISHER_ID=2896
|
||||||
//BUILD_COMMIT=0000000
|
#BUILD_COMMIT=0000000
|
||||||
|
ADMIN_AUTH_URL=http://localhost/admin/authenticate?token=$1
|
||||||
@ -1,21 +0,0 @@
|
|||||||
DEV README von Alex
|
|
||||||
|
|
||||||
default Page:
|
|
||||||
´´´
|
|
||||||
<template>
|
|
||||||
<div>default</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'default',
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
methods: {},
|
|
||||||
watch: {},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
´´´
|
|
||||||
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<!--
|
|
||||||
IMPORTANT: Please use the following link to create a new issue:
|
|
||||||
|
|
||||||
https://www.gradido.net/new-issue/bootstrap-vue-gradido-wallet
|
|
||||||
|
|
||||||
**If your issue was not created using the app above, it will be closed immediately.**
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
@ -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
|
GET https://staging.gradido.net/state-balances/ajaxGdtTransactions
|
||||||
Liefert wenn alles in Ordnung ist:
|
Liefert wenn alles in Ordnung ist:
|
||||||
|
|
||||||
wenn nicht type 7 dann "amount" in euro ansonsten in GDT
|
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
|
||||||
|
```
|
||||||
@ -3,6 +3,8 @@ import SideBar from './SideBar'
|
|||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const storeDispatchMock = jest.fn()
|
||||||
|
|
||||||
describe('SideBar', () => {
|
describe('SideBar', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ describe('SideBar', () => {
|
|||||||
lastName: 'example',
|
lastName: 'example',
|
||||||
hasElopage: false,
|
hasElopage: false,
|
||||||
},
|
},
|
||||||
commit: jest.fn(),
|
dispatch: storeDispatchMock,
|
||||||
},
|
},
|
||||||
$i18n: {
|
$i18n: {
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
@ -154,6 +156,42 @@ describe('SideBar', () => {
|
|||||||
expect(wrapper.emitted('logout')).toEqual([[]])
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -45,11 +45,20 @@
|
|||||||
<slot name="links"></slot>
|
<slot name="links"></slot>
|
||||||
</ul>
|
</ul>
|
||||||
<hr class="my-2" />
|
<hr class="my-2" />
|
||||||
|
|
||||||
<ul class="navbar-nav ml-3">
|
<ul class="navbar-nav ml-3">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a :href="getElopageLink()" class="nav-link" target="_blank">
|
<a :href="getElopageLink()" class="nav-link" target="_blank">
|
||||||
{{ $t('members_area') }}
|
{{ $t('members_area') }}
|
||||||
<b-badge v-if="!this.$store.state.hasElopage" pill variant="danger">!</b-badge>
|
<b-badge v-if="!$store.state.hasElopage" pill variant="danger">!</b-badge>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul class="navbar-nav ml-3" v-if="$store.state.isAdmin">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link pointer" @click="admin">
|
||||||
|
{{ $t('admin_area') }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -112,6 +121,10 @@ export default {
|
|||||||
logout() {
|
logout() {
|
||||||
this.$emit('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() {
|
getElopageLink() {
|
||||||
const pId = this.$store.state.publisherId
|
const pId = this.$store.state.publisherId
|
||||||
? this.$store.state.publisherId
|
? this.$store.state.publisherId
|
||||||
|
|||||||
@ -18,8 +18,9 @@ const environment = {
|
|||||||
DEFAULT_PUBLISHER_ID: process.env.DEFAULT_PUBLISHER_ID || 2896,
|
DEFAULT_PUBLISHER_ID: process.env.DEFAULT_PUBLISHER_ID || 2896,
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = {
|
const endpoints = {
|
||||||
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000/graphql',
|
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 = {}
|
const options = {}
|
||||||
@ -27,7 +28,7 @@ const options = {}
|
|||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
...version,
|
...version,
|
||||||
...environment,
|
...environment,
|
||||||
...server,
|
...endpoints,
|
||||||
...options,
|
...options,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,27 @@ export const login = gql`
|
|||||||
}
|
}
|
||||||
hasElopage
|
hasElopage
|
||||||
publisherId
|
publisherId
|
||||||
|
isAdmin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const verifyLogin = gql`
|
||||||
|
query {
|
||||||
|
verifyLogin {
|
||||||
|
email
|
||||||
|
username
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
language
|
||||||
|
description
|
||||||
|
coinanimation
|
||||||
|
klickTipp {
|
||||||
|
newsletterState
|
||||||
|
}
|
||||||
|
hasElopage
|
||||||
|
publisherId
|
||||||
|
isAdmin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"admin_area": "Adminbereich",
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
"community": {
|
"community": {
|
||||||
"choose-another-community": "Eine andere Gemeinschaft auswählen",
|
"choose-another-community": "Eine andere Gemeinschaft auswählen",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"admin_area": "Admin's area",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"community": {
|
"community": {
|
||||||
"choose-another-community": "Choose another community",
|
"choose-another-community": "Choose another community",
|
||||||
|
|||||||
@ -51,7 +51,7 @@ Vue.config.productionTip = false
|
|||||||
|
|
||||||
loadAllRules(i18n)
|
loadAllRules(i18n)
|
||||||
|
|
||||||
addNavigationGuards(router, store)
|
addNavigationGuards(router, store, apolloProvider.defaultClient)
|
||||||
|
|
||||||
/* eslint-disable no-new */
|
/* eslint-disable no-new */
|
||||||
new Vue({
|
new Vue({
|
||||||
|
|||||||
@ -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) => {
|
router.beforeEach((to, from, next) => {
|
||||||
// handle publisherId
|
|
||||||
const publisherId = to.query.pid
|
const publisherId = to.query.pid
|
||||||
if (publisherId) {
|
if (publisherId) {
|
||||||
store.commit('publisherId', publisherId)
|
store.commit('publisherId', publisherId)
|
||||||
delete to.query.pid
|
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) {
|
if (to.meta.requiresAuth && !store.state.token) {
|
||||||
next({ path: '/login' })
|
next({ path: '/login' })
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ describe('navigation guards', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authorization', () => {
|
describe('authorization', () => {
|
||||||
const navGuard = router.beforeHooks[0]
|
const navGuard = router.beforeHooks[2]
|
||||||
const next = jest.fn()
|
const next = jest.fn()
|
||||||
|
|
||||||
it('redirects to login when not authorized', () => {
|
it('redirects to login when not authorized', () => {
|
||||||
|
|||||||
@ -49,8 +49,8 @@ describe('router', () => {
|
|||||||
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
|
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has fourteen routes defined', () => {
|
it('has fifteen routes defined', () => {
|
||||||
expect(routes).toHaveLength(14)
|
expect(routes).toHaveLength(15)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('overview', () => {
|
describe('overview', () => {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import NotFound from '@/views/NotFoundPage.vue'
|
import NotFound from '@/views/NotFoundPage.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/authenticate',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: (to) => {
|
redirect: (to) => {
|
||||||
|
|||||||
@ -34,6 +34,9 @@ export const mutations = {
|
|||||||
if (isNaN(pubId)) pubId = null
|
if (isNaN(pubId)) pubId = null
|
||||||
state.publisherId = pubId
|
state.publisherId = pubId
|
||||||
},
|
},
|
||||||
|
isAdmin: (state, isAdmin) => {
|
||||||
|
state.isAdmin = !!isAdmin
|
||||||
|
},
|
||||||
community: (state, community) => {
|
community: (state, community) => {
|
||||||
state.community = community
|
state.community = community
|
||||||
},
|
},
|
||||||
@ -57,6 +60,7 @@ export const actions = {
|
|||||||
commit('newsletterState', data.klickTipp.newsletterState)
|
commit('newsletterState', data.klickTipp.newsletterState)
|
||||||
commit('hasElopage', data.hasElopage)
|
commit('hasElopage', data.hasElopage)
|
||||||
commit('publisherId', data.publisherId)
|
commit('publisherId', data.publisherId)
|
||||||
|
commit('isAdmin', data.isAdmin)
|
||||||
},
|
},
|
||||||
logout: ({ commit, state }) => {
|
logout: ({ commit, state }) => {
|
||||||
commit('token', null)
|
commit('token', null)
|
||||||
@ -69,6 +73,7 @@ export const actions = {
|
|||||||
commit('newsletterState', null)
|
commit('newsletterState', null)
|
||||||
commit('hasElopage', false)
|
commit('hasElopage', false)
|
||||||
commit('publisherId', null)
|
commit('publisherId', null)
|
||||||
|
commit('isAdmin', false)
|
||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -87,6 +92,7 @@ export const store = new Vuex.Store({
|
|||||||
username: '',
|
username: '',
|
||||||
description: '',
|
description: '',
|
||||||
token: null,
|
token: null,
|
||||||
|
isAdmin: false,
|
||||||
coinanimation: true,
|
coinanimation: true,
|
||||||
newsletterState: null,
|
newsletterState: null,
|
||||||
community: {
|
community: {
|
||||||
|
|||||||
@ -148,11 +148,12 @@ describe('Vuex store', () => {
|
|||||||
},
|
},
|
||||||
hasElopage: false,
|
hasElopage: false,
|
||||||
publisherId: 1234,
|
publisherId: 1234,
|
||||||
|
isAdmin: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
it('calls ten commits', () => {
|
it('calls eleven commits', () => {
|
||||||
login({ commit, state }, commitedData)
|
login({ commit, state }, commitedData)
|
||||||
expect(commit).toHaveBeenCalledTimes(10)
|
expect(commit).toHaveBeenCalledTimes(11)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('commits email', () => {
|
it('commits email', () => {
|
||||||
@ -204,15 +205,20 @@ describe('Vuex store', () => {
|
|||||||
login({ commit, state }, commitedData)
|
login({ commit, state }, commitedData)
|
||||||
expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', 1234)
|
expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', 1234)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('commits isAdmin', () => {
|
||||||
|
login({ commit, state }, commitedData)
|
||||||
|
expect(commit).toHaveBeenNthCalledWith(11, 'isAdmin', true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('logout', () => {
|
describe('logout', () => {
|
||||||
const commit = jest.fn()
|
const commit = jest.fn()
|
||||||
const state = {}
|
const state = {}
|
||||||
|
|
||||||
it('calls ten commits', () => {
|
it('calls eleven commits', () => {
|
||||||
logout({ commit, state })
|
logout({ commit, state })
|
||||||
expect(commit).toHaveBeenCalledTimes(10)
|
expect(commit).toHaveBeenCalledTimes(11)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('commits token', () => {
|
it('commits token', () => {
|
||||||
@ -265,6 +271,11 @@ describe('Vuex store', () => {
|
|||||||
expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', null)
|
expect(commit).toHaveBeenNthCalledWith(10, 'publisherId', null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('commits isAdmin', () => {
|
||||||
|
logout({ commit, state })
|
||||||
|
expect(commit).toHaveBeenNthCalledWith(11, 'isAdmin', false)
|
||||||
|
})
|
||||||
|
|
||||||
// how to get this working?
|
// how to get this working?
|
||||||
it.skip('calls localStorage.clear()', () => {
|
it.skip('calls localStorage.clear()', () => {
|
||||||
const clearStorageMock = jest.fn()
|
const clearStorageMock = jest.fn()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user