Merge branch 'master' into login_admin_interface

This commit is contained in:
Moriz Wahl 2021-11-24 18:23:31 +01:00
commit ac7eb303b4
50 changed files with 1144 additions and 228 deletions

View File

@ -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: 47
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################
@ -491,7 +491,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 38 min_coverage: 37
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -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',

View File

@ -35,6 +35,7 @@
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "26.6.3", "jest": "26.6.3",
"moment": "^2.29.1", "moment": "^2.29.1",
"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",

26
admin/src/App.spec.js Normal file
View File

@ -0,0 +1,26 @@
import { shallowMount } from '@vue/test-utils'
import App from './App'
const localVue = global.localVue
const stubs = {
RouterView: true,
}
describe('App', () => {
let wrapper
const Wrapper = () => {
return shallowMount(App, { localVue, stubs })
}
describe('shallowMount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has a div with id "app"', () => {
expect(wrapper.find('div#app').exists()).toBeTruthy()
})
})
})

View File

@ -8,3 +8,8 @@
</div> </div>
</div> </div>
</template> </template>
<script>
export default {
name: 'ContentFooter',
}
</script>

View File

@ -19,7 +19,7 @@ const mocks = {
const propsData = { const propsData = {
type: '', type: '',
item: {}, item: {},
creation: {}, creation: [],
itemsMassCreation: {}, itemsMassCreation: {},
} }
@ -38,5 +38,104 @@ describe('CreationFormular', () => {
it('has a DIV element with the class.component-creation-formular', () => { it('has a DIV element with the class.component-creation-formular', () => {
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy() 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)
})
})
})
})
}) })
}) })

View File

@ -4,9 +4,10 @@
<h3> <h3>
{{ {{
this.type === 'singleCreation' this.type === 'singleCreation'
? 'Einzelschöpfung für ' + item.first_name + ' ' + item.last_name + '' ? 'Einzelschöpfung für ' + item.firstName + ' ' + item.lastName + ''
: 'Massenschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder' : 'Mehrfachschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder'
}} }}
{{ item }}
</h3> </h3>
<div v-show="this.type === 'massCreation' && Object.keys(this.itemsMassCreation).length <= 0"> <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 Bitte wähle ein oder Mehrere Mitglieder aus für die du Schöpfen möchtest
@ -26,7 +27,7 @@
size="lg" size="lg"
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])" @change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
> >
{{ beforeLastMonth }} {{ creation[0] != null ? creation[0] + ' GDD' : '' }} {{ beforeLastMonth.short }} {{ creation[0] != null ? creation[0] + ' GDD' : '' }}
</b-form-radio> </b-form-radio>
</b-col> </b-col>
<b-col> <b-col>
@ -36,7 +37,7 @@
size="lg" size="lg"
@change="updateRadioSelected(lastMonth, 1, creation[1])" @change="updateRadioSelected(lastMonth, 1, creation[1])"
> >
{{ lastMonth }} {{ creation[1] != null ? creation[1] + ' GDD' : '' }} {{ lastMonth.short }} {{ creation[1] != null ? creation[1] + ' GDD' : '' }}
</b-form-radio> </b-form-radio>
</b-col> </b-col>
<b-col class="text-right"> <b-col class="text-right">
@ -46,7 +47,7 @@
size="lg" size="lg"
@change="updateRadioSelected(currentMonth, 2, creation[2])" @change="updateRadioSelected(currentMonth, 2, creation[2])"
> >
{{ currentMonth }} {{ creation[2] != null ? creation[2] + ' GDD' : '' }} {{ currentMonth.short }} {{ creation[2] != null ? creation[2] + ' GDD' : '' }}
</b-form-radio> </b-form-radio>
</b-col> </b-col>
</b-row> </b-row>
@ -73,6 +74,7 @@
:min="rangeMin" :min="rangeMin"
:max="rangeMax" :max="rangeMax"
step="10" step="10"
@load="checkFormForUpdate('range')"
></b-input> ></b-input>
</b-row> </b-row>
<b-row class="m-4"> <b-row class="m-4">
@ -83,6 +85,7 @@
v-model="text" v-model="text"
:state="text.length >= 10" :state="text.length >= 10"
placeholder="Mindestens 10 Zeichen eingeben" placeholder="Mindestens 10 Zeichen eingeben"
@load="checkFormForUpdate('text')"
rows="3" rows="3"
></b-form-textarea> ></b-form-textarea>
</div> </div>
@ -96,6 +99,17 @@
<b-col class="text-center"> <b-col class="text-center">
<div class="text-right"> <div class="text-right">
<b-button <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" type="button"
variant="success" variant="success"
@click="submitCreation" @click="submitCreation"
@ -116,14 +130,23 @@ export default {
props: { props: {
type: { type: {
type: String, type: String,
required: true, required: false,
},
pagetype: {
type: String,
required: false,
default: '',
}, },
item: { item: {
type: Object, type: Object,
required: false, required: false,
}, },
creation: { creationUserData: {
type: Object, type: Object,
required: false,
},
creation: {
type: Array,
required: true, required: true,
}, },
itemsMassCreation: { itemsMassCreation: {
@ -138,9 +161,18 @@ export default {
value: 0, value: 0,
rangeMin: 0, rangeMin: 0,
rangeMax: 1000, rangeMax: 1000,
currentMonth: this.$moment().format('MMMM'), currentMonth: {
lastMonth: this.$moment().subtract(1, 'month').format('MMMM'), short: this.$moment().format('MMMM'),
beforeLastMonth: this.$moment().subtract(2, 'month').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, submitObj: null,
isdisabled: true, isdisabled: true,
} }
@ -148,7 +180,7 @@ export default {
methods: { methods: {
// Auswählen eines Zeitraumes // Auswählen eines Zeitraumes
updateRadioSelected(name, index, openCreation) { updateRadioSelected(name, index, openCreation) {
// Wenn Massenschöpfung // Wenn Mehrfachschöpfung
if (this.type === 'massCreation') { if (this.type === 'massCreation') {
// An Creation.vue emitten und radioSelectedMass aktualisieren // An Creation.vue emitten und radioSelectedMass aktualisieren
this.$emit('update-radio-selected', [name, index]) this.$emit('update-radio-selected', [name, index])
@ -160,6 +192,19 @@ export default {
this.rangeMax = openCreation 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() { submitCreation() {
// Formular Prüfen ob ein Zeitraum ausgewählt wurde. Ansonsten abbrechen und Hinweis anzeigen // Formular Prüfen ob ein Zeitraum ausgewählt wurde. Ansonsten abbrechen und Hinweis anzeigen
if (this.radioSelected === '') { if (this.radioSelected === '') {
@ -178,9 +223,9 @@ export default {
return alert('Bitte gib einen Text ein der länger als 10 Zeichen ist!') return alert('Bitte gib einen Text ein der länger als 10 Zeichen ist!')
} }
if (this.type === 'massCreation') { if (this.type === 'massCreation') {
// Die anzahl der Mitglieder aus der Massenschöpfung // Die anzahl der Mitglieder aus der Mehrfachschöpfung
const i = Object.keys(this.itemsMassCreation).length const i = Object.keys(this.itemsMassCreation).length
// hinweis das eine Massenschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird) // 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') alert('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
this.submitObj = [ this.submitObj = [
{ {
@ -191,32 +236,44 @@ export default {
moderator: this.$store.state.moderator, moderator: this.$store.state.moderator,
}, },
] ]
alert('MassenSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder') alert('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
// $store - offene Schöpfungen hochzählen // $store - offene Schöpfungen hochzählen
this.$store.commit('openCreationsPlus', i) this.$store.commit('openCreationsPlus', i)
// lösche alle Mitglieder aus der MassenSchöpfungsListe nach dem alle Massenschpfungen zum bestätigen gesendet wurden. // lösche alle Mitglieder aus der MehrfachSchöpfungsListe nach dem alle Mehrfachschpfungen zum bestätigen gesendet wurden.
this.$emit('remove-all-bookmark') this.$emit('remove-all-bookmark')
} }
if (this.type === 'singleCreation') { if (this.type === 'singleCreation') {
// hinweis das eine einzelne schöpfung ausgeführt wird an (Vorname) // hinweis das eine einzelne schöpfung ausgeführt wird an (Vorname)
alert('SUBMIT CREATION => ' + this.type + ' >> für ' + this.item.first_name + '') alert('SUBMIT CREATION => ' + this.type + ' >> für ' + this.item.firstName + '')
// erstellen eines Arrays (submitObj) mit allen Daten // erstellen eines Arrays (submitObj) mit allen Daten
this.submitObj = [ this.submitObj = [
{ {
item: this.item, item: this.item,
datum: this.radioSelected, datum: this.radioSelected.long,
amount: this.value, amount: this.value,
text: this.text, text: this.text,
moderator: this.$store.state.moderator, moderator: this.$store.state.moderator,
}, },
] ]
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
alert('EINZEL SCHÖPFUNG ABSENDEN FÜR >> ' + this.item.first_name + '') if (this.pagetype === 'PageCreationConfirm') {
// $store - offene Schöpfungen hochzählen // hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
this.$store.commit('openCreationsPlus', 1) 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 // das absendeergebniss im string ansehen

View File

@ -15,7 +15,7 @@ describe('NavBar', () => {
let wrapper let wrapper
const Wrapper = () => { const Wrapper = () => {
return mount(NavBar, { localVue, mocks }) return mount(NavBar, { mocks, localVue })
} }
describe('mount', () => { describe('mount', () => {

View File

@ -8,7 +8,7 @@
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/user">Usersuche |</b-nav-item> <b-nav-item to="/user">Usersuche |</b-nav-item>
<b-nav-item to="/creation">Massenschöpfung</b-nav-item> <b-nav-item to="/creation">Mehrfachschöpfung</b-nav-item>
<b-nav-item <b-nav-item
v-show="$store.state.openCreations > 0" v-show="$store.state.openCreations > 0"
class="h5 bg-danger" class="h5 bg-danger"

View File

@ -10,7 +10,7 @@ describe('UserTable', () => {
type: 'Type', type: 'Type',
itemsUser: [], itemsUser: [],
fieldsTable: [], fieldsTable: [],
creation: {}, creation: [],
} }
const Wrapper = () => { const Wrapper = () => {

View File

@ -37,7 +37,12 @@
stacked="md" stacked="md"
> >
<template #cell(edit_creation)="row"> <template #cell(edit_creation)="row">
<b-button variant="info" size="lg" @click="row.toggleDetails" class="mr-2"> <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-if="row.detailsShowing" icon="x" aria-label="Help"></b-icon>
<b-icon v-else icon="pencil-square" aria-label="Help"></b-icon> <b-icon v-else icon="pencil-square" aria-label="Help"></b-icon>
</b-button> </b-button>
@ -46,7 +51,7 @@
<template #cell(show_details)="row"> <template #cell(show_details)="row">
<b-button variant="info" size="lg" @click="row.toggleDetails" class="mr-2"> <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-if="row.detailsShowing" icon="eye-slash-fill" aria-label="Help"></b-icon>
<b-icon v-else icon="eye-slash-fill" aria-label="Help"></b-icon> <b-icon v-else icon="eye-fill" aria-label="Help"></b-icon>
</b-button> </b-button>
</template> </template>
@ -58,8 +63,11 @@
<creation-formular <creation-formular
type="singleCreation" type="singleCreation"
:creation="getCreationInMonths(row.item.creation)" :pagetype="type"
:creation="row.item.creation"
:item="row.item" :item="row.item"
:creationUserData="creationData"
@update-creation-data="updateCreationData"
/> />
<b-button size="sm" @click="row.toggleDetails"> <b-button size="sm" @click="row.toggleDetails">
@ -67,7 +75,7 @@
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'" :icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
aria-label="Help" aria-label="Help"
></b-icon> ></b-icon>
Details verbergen von {{ row.item.first_name }} {{ row.item.last_name }} Details verbergen von {{ row.item.firstName }} {{ row.item.lastName }}
</b-button> </b-button>
</b-card> </b-card>
</template> </template>
@ -132,7 +140,7 @@ export default {
default: '', default: '',
}, },
creation: { creation: {
type: Object, type: Array,
required: false, required: false,
}, },
}, },
@ -141,6 +149,7 @@ export default {
}, },
data() { data() {
return { return {
creationData: {},
overlay: false, overlay: false,
overlayBookmarkType: '', overlayBookmarkType: '',
overlayItem: [], overlayItem: [],
@ -200,16 +209,28 @@ export default {
} }
if (this.type === 'PageCreationConfirm') { if (this.type === 'PageCreationConfirm') {
this.$emit('update-confirm-result', item, 'remove') this.$emit('remove-confirm-result', item, 'remove')
} }
}, },
bookmarkConfirm(item) { bookmarkConfirm(item) {
alert('die schöpfung bestätigen und abschließen') alert('die schöpfung bestätigen und abschließen')
alert(JSON.stringify(item)) alert(JSON.stringify(item))
this.$emit('update-confirm-result', item, 'remove') this.$emit('remove-confirm-result', item, 'remove')
}, },
getCreationInMonths(creation) { editCreationUserTable(row, rowItem) {
return creation.split(',') 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,
}
}, },
}, },
} }

View 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
View 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(),
}),
)
})
})

View File

@ -1,6 +1,9 @@
import Vue from 'vue' import Vue from 'vue'
import App from './App.vue' import App from './App.vue'
// without this async calls are not working
import 'regenerator-runtime'
import store from './store/store' import store from './store/store'
import router from './router/router' import router from './router/router'
@ -22,7 +25,8 @@ 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,10 +56,13 @@ const apolloProvider = new VueApollo({
}) })
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
Vue.use(IconsPlugin) Vue.use(IconsPlugin)
Vue.use(moment) Vue.use(moment)
Vue.use(VueApollo)
addNavigationGuards(router, store) addNavigationGuards(router, store)
new Vue({ new Vue({

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="creation">
<b-row> <b-row>
<b-col cols="12" lg="5"> <b-col cols="12" lg="5">
<label>Usersuche</label> <label>Usersuche</label>
@ -10,6 +10,7 @@
placeholder="User suche" placeholder="User suche"
></b-input> ></b-input>
<user-table <user-table
v-if="itemsList.length > 0"
type="UserListSearch" type="UserListSearch"
:itemsUser="itemsList" :itemsUser="itemsList"
:fieldsTable="Searchfields" :fieldsTable="Searchfields"
@ -20,7 +21,7 @@
</b-col> </b-col>
<b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info"> <b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info">
<user-table <user-table
v-show="Object.keys(this.massCreation).length > 0" v-if="massCreation.length > 0"
class="shadow p-3 mb-5 bg-white rounded" class="shadow p-3 mb-5 bg-white rounded"
type="UserListMassCreation" type="UserListMassCreation"
:itemsUser="massCreation" :itemsUser="massCreation"
@ -31,6 +32,7 @@
/> />
<creation-formular <creation-formular
v-if="massCreation.length > 0"
type="massCreation" type="massCreation"
:creation="creation" :creation="creation"
:itemsMassCreation="massCreation" :itemsMassCreation="massCreation"
@ -44,9 +46,10 @@
<script> <script>
import CreationFormular from '../components/CreationFormular.vue' import CreationFormular from '../components/CreationFormular.vue'
import UserTable from '../components/UserTable.vue' import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
export default { export default {
name: 'overview', name: 'Creation',
components: { components: {
CreationFormular, CreationFormular,
UserTable, UserTable,
@ -56,60 +59,49 @@ export default {
showArrays: false, showArrays: false,
Searchfields: [ Searchfields: [
{ key: 'bookmark', label: 'merken' }, { key: 'bookmark', label: 'merken' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'first_name', label: 'Firstname' }, { key: 'lastName', label: 'Lastname' },
{ key: 'last_name', label: 'Lastname' },
{ key: 'creation', label: 'Creation' }, { key: 'creation', label: 'Creation' },
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
], ],
fields: [ fields: [
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
{ key: 'first_name', label: 'Firstname' }, { key: 'firstName', label: 'Firstname' },
{ key: 'last_name', label: 'Lastname' }, { key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' }, { key: 'creation', label: 'Creation' },
{ key: 'bookmark', label: 'löschen' }, { key: 'bookmark', label: 'löschen' },
], ],
searchResult: [ itemsList: [],
{
id: 1,
email: 'dickerson@web.de',
first_name: 'Dickerson',
last_name: 'Macdonald',
creation: '450,200,700',
},
{
id: 2,
email: 'larsen@woob.de',
first_name: 'Larsen',
last_name: 'Shaw',
creation: '300,200,1000',
},
{
id: 3,
email: 'geneva@tete.de',
first_name: 'Geneva',
last_name: 'Wilson',
creation: '350,200,900',
},
{
id: 4,
email: 'viewrter@asdfvb.com',
first_name: 'Soledare',
last_name: 'Takker',
creation: '100,400,800',
},
],
itemsList: this.searchResult,
massCreation: [], massCreation: [],
radioSelectedMass: '', radioSelectedMass: '',
criteria: '', criteria: '',
creation: [null, null, null], creation: [null, null, null],
} }
}, },
created() { async created() {
this.itemsList = this.searchResult await this.getUsers()
}, },
methods: { 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) { updateItem(e, event) {
let index = 0 let index = 0
let findArr = {} let findArr = {}

View File

@ -1,5 +1,5 @@
<template> <template>
<div> <div class="creation-confirm">
<small class="bg-danger text-light p-1"> <small class="bg-danger text-light p-1">
Die anzahl der offene Schöpfungen stimmen nicht! Diese wird bei absenden im $store Die anzahl der offene Schöpfungen stimmen nicht! Diese wird bei absenden im $store
hochgezählt. Die Liste die hier angezeigt wird ist SIMULIERT! hochgezählt. Die Liste die hier angezeigt wird ist SIMULIERT!
@ -8,9 +8,8 @@
class="mt-4" class="mt-4"
type="PageCreationConfirm" type="PageCreationConfirm"
:itemsUser="confirmResult" :itemsUser="confirmResult"
:creation="creation"
:fieldsTable="fields" :fieldsTable="fields"
@update-confirm-result="updateConfirmResult" @remove-confirm-result="removeConfirmResult"
/> />
</div> </div>
</template> </template>
@ -18,7 +17,7 @@
import UserTable from '../components/UserTable.vue' import UserTable from '../components/UserTable.vue'
export default { export default {
name: 'creation_confirm', name: 'CreationConfirm',
components: { components: {
UserTable, UserTable,
}, },
@ -28,11 +27,23 @@ export default {
fields: [ fields: [
{ key: 'bookmark', label: 'löschen' }, { key: 'bookmark', label: 'löschen' },
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
{ key: 'first_name', label: 'Vorname' }, { key: 'firstName', label: 'Vorname' },
{ key: 'last_name', label: 'Nachname' }, { key: 'lastName', label: 'Nachname' },
{ key: 'creation_gdd', label: 'GDD' }, {
key: 'creation_gdd',
label: 'Schöpfung',
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'text', label: 'Text' }, { key: 'text', label: 'Text' },
{ key: 'creation_date', label: 'Datum' }, {
key: 'creation_date',
label: 'Datum',
formatter: (value) => {
return value.long
},
},
{ key: 'creation_moderator', label: 'Moderator' }, { key: 'creation_moderator', label: 'Moderator' },
{ key: 'edit_creation', label: 'ändern' }, { key: 'edit_creation', label: 'ändern' },
{ key: 'confirm', label: 'speichern' }, { key: 'confirm', label: 'speichern' },
@ -41,67 +52,81 @@ export default {
{ {
id: 1, id: 1,
email: 'dickerson@web.de', email: 'dickerson@web.de',
first_name: 'Dickerson', firstName: 'Dickerson',
last_name: 'Macdonald', lastName: 'Macdonald',
creation: '450,200,700', creation: '[450,200,700]',
creation_gdd: '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 ', text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam ',
creation_date: '01/11/2021', creation_date: {
short: 'November',
long: '22/11/2021',
},
creation_moderator: 'Manuela Gast', creation_moderator: 'Manuela Gast',
}, },
{ {
id: 2, id: 2,
email: 'larsen@woob.de', email: 'larsen@woob.de',
first_name: 'Larsen', firstName: 'Larsen',
last_name: 'Shaw', lastName: 'Shaw',
creation: '300,200,1000', creation: '[300,200,1000]',
creation_gdd: '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 ', text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam ',
creation_date: '01/11/2021', creation_date: {
short: 'November',
long: '03/11/2021',
},
creation_moderator: 'Manuela Gast', creation_moderator: 'Manuela Gast',
}, },
{ {
id: 3, id: 3,
email: 'geneva@tete.de', email: 'geneva@tete.de',
first_name: 'Geneva', firstName: 'Geneva',
last_name: 'Wilson', lastName: 'Wilson',
creation: '350,200,900', creation: '[350,200,900]',
creation_gdd: '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', text: 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam',
creation_date: '01/11/2021', creation_date: {
short: 'September',
long: '27/09/2021',
},
creation_moderator: 'Manuela Gast', creation_moderator: 'Manuela Gast',
}, },
{ {
id: 4, id: 4,
email: 'viewrter@asdfvb.com', email: 'viewrter@asdfvb.com',
first_name: 'Soledare', firstName: 'Soledare',
last_name: 'Takker', lastName: 'Takker',
creation: '[100,400,800]',
creation_gdd: '500', 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 ', 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: {
creation_date: '01/10/2021', short: 'Oktober',
long: '12/10/2021',
},
creation_moderator: 'Evelyn Roller', creation_moderator: 'Evelyn Roller',
}, },
{ {
id: 5, id: 5,
email: 'dickerson@web.de', email: 'dickerson@web.de',
first_name: 'Dickerson', firstName: 'Dickerson',
last_name: 'Macdonald', lastName: 'Macdonald',
creation: '100,400,800', creation: '[100,400,800]',
creation_gdd: '200', 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', 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: '01/09/2021', creation_date: {
short: 'September',
long: '05/09/2021',
},
creation_moderator: 'Manuela Gast', creation_moderator: 'Manuela Gast',
}, },
], ],
creation: [null, null, null],
} }
}, },
methods: { methods: {
updateConfirmResult(e, event) { removeConfirmResult(e, event) {
if (event === 'remove') { if (event === 'remove') {
let index = 0 let index = 0
let findArr = {} let findArr = {}

View File

@ -1,11 +1,12 @@
<template> <template>
<div> <div class="user-search">
<label>Usersuche</label> <label>Usersuche</label>
<b-input <b-input
type="text" type="text"
v-model="criteria" v-model="criteria"
class="shadow p-3 mb-5 bg-white rounded" class="shadow p-3 mb-5 bg-white rounded"
placeholder="User suche" placeholder="User suche"
@input="getUsers"
></b-input> ></b-input>
<user-table <user-table
type="PageUserSearch" type="PageUserSearch"
@ -17,9 +18,10 @@
</template> </template>
<script> <script>
import UserTable from '../components/UserTable.vue' import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
export default { export default {
name: 'overview', name: 'UserSearch',
components: { components: {
UserTable, UserTable,
}, },
@ -28,46 +30,41 @@ export default {
showArrays: false, showArrays: false,
fields: [ fields: [
{ key: 'email', label: 'Email' }, { key: 'email', label: 'Email' },
{ key: 'first_name', label: 'Firstname' }, { key: 'firstName', label: 'Firstname' },
{ key: 'last_name', label: 'Lastname' }, { key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' }, { key: 'creation', label: 'Creation' },
{ key: 'show_details', label: 'Details' }, { key: 'show_details', label: 'Details' },
], ],
searchResult: [ searchResult: [],
{
id: 1,
email: 'dickerson@web.de',
first_name: 'Dickerson',
last_name: 'Macdonald',
creation: '450,200,700',
},
{
id: 2,
email: 'larsen@woob.de',
first_name: 'Larsen',
last_name: 'Shaw',
creation: '300,200,1000',
},
{
id: 3,
email: 'geneva@tete.de',
first_name: 'Geneva',
last_name: 'Wilson',
creation: '350,200,900',
},
{
id: 4,
email: 'viewrter@asdfvb.com',
first_name: 'Soledare',
last_name: 'Takker',
creation: '100,400,800',
},
],
massCreation: [], massCreation: [],
criteria: '', criteria: '',
} }
}, },
methods: {}, 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> </script>

View 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 "/overview" as default', async () => {
const component = await routes.find((r) => r.path === '/').component()
expect(component.default.name).toBe('overview')
})
it('has fourteen routes defined', () => {
expect(routes).toHaveLength(6)
})
describe('overview', () => {
it('loads the "Overview" component', async () => {
const component = await routes.find((r) => r.path === '/overview').component()
expect(component.default.name).toBe('overview')
})
})
describe('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')
})
})
})
})
})

View File

@ -7,10 +7,10 @@ Vue.use(Vuex)
export const mutations = { export const mutations = {
openCreationsPlus: (state, i) => { openCreationsPlus: (state, i) => {
state.openCreations = state.openCreations + i state.openCreations += i
}, },
openCreationsMinus: (state, i) => { openCreationsMinus: (state, i) => {
state.openCreations = state.openCreations - i state.openCreations -= i
}, },
resetOpenCreations: (state) => { resetOpenCreations: (state) => {
state.openCreations = 0 state.openCreations = 0

View File

@ -0,0 +1,39 @@
import { mutations } from './store'
const { token, openCreationsPlus, openCreationsMinus, resetOpenCreations } = mutations
describe('Vuex store', () => {
describe('mutations', () => {
describe('token', () => {
it('sets the state of token', () => {
const state = { token: null }
token(state, '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)
})
})
})
})

View 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')
})
})
})
})

View 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)
})
})
})
})

View 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')
})
})
})
})

View File

@ -1,10 +1,14 @@
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
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) => {

View File

@ -10623,7 +10623,7 @@ regenerator-runtime@^0.11.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4: regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9:
version "0.13.9" version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==

View File

@ -30,4 +30,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

View File

@ -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",

View File

@ -51,14 +51,25 @@ const email = {
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx', EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com', EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_LINK_VERIFICATION: EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1', process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
} }
const webhook = {
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
// This is needed by graphql-directive-auth // 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

View File

@ -12,10 +12,7 @@ export default class CreateUserArgs {
lastName: string lastName: string
@Field(() => String) @Field(() => String)
password: string language?: string // Will default to DEFAULT_LANGUAGE
@Field(() => String)
language: string
@Field(() => Int, { nullable: true }) @Field(() => Int, { nullable: true })
publisherId: number publisherId: number

View File

@ -2,9 +2,6 @@
import { AuthChecker } from 'type-graphql' import { AuthChecker } from 'type-graphql'
import CONFIG from '../../config'
import { apiGet } from '../../apis/HttpRequest'
import decode from '../../jwt/decode' import decode from '../../jwt/decode'
import encode from '../../jwt/encode' import encode from '../../jwt/encode'
@ -13,7 +10,7 @@ const isAuthorized: AuthChecker<any> = async (
) => { ) => {
if (context.token) { if (context.token) {
const decoded = decode(context.token) const decoded = decode(context.token)
context.pubKey = decoded.pubKey context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) }) context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
return true return true
} }

View 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[]
}

View 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
}
}

View File

@ -613,9 +613,6 @@ export class TransactionResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw e
} finally {
await queryRunner.release()
// TODO: This is broken code - we should never correct an autoincrement index in production // TODO: This is broken code - we should never correct an autoincrement index in production
// according to dario it is required tho to properly work. The index of the table is used as // according to dario it is required tho to properly work. The index of the table is used as
// index for the transaction which requires a chain without gaps // index for the transaction which requires a chain without gaps
@ -627,6 +624,9 @@ export class TransactionResolver {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('problems with reset auto increment: %o', error) console.log('problems with reset auto increment: %o', error)
}) })
throw e
} finally {
await queryRunner.release()
} }
// send notification email // send notification email
// TODO: translate // TODO: translate

View File

@ -22,14 +22,14 @@ import {
} from '../../middleware/klicktippMiddleware' } from '../../middleware/klicktippMiddleware'
import { CheckEmailResponse } from '../model/CheckEmailResponse' import { CheckEmailResponse } from '../model/CheckEmailResponse'
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository' import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { Setting } from '../enum/Setting' import { Setting } from '../enum/Setting'
import { UserRepository } from '../../typeorm/repository/User' import { UserRepository } from '../../typeorm/repository/User'
import { LoginUser } from '@entity/LoginUser' import { LoginUser } from '@entity/LoginUser'
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
import { LoginUserBackup } from '@entity/LoginUserBackup' import { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendEMail } from '../../util/sendEMail' import { sendEMail } from '../../util/sendEMail'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser' import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native') const sodium = require('sodium-native')
@ -231,33 +231,33 @@ export class UserResolver {
@Ctx() context: any, @Ctx() context: any,
): Promise<User> { ): Promise<User> {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password }) // const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password })
// UnsecureLogin
// if there is no user, throw an authentication error const loginUserRepository = getCustomRepository(LoginUserRepository)
if (!result.success) { const loginUser = await loginUserRepository.findByEmail(email).catch(() => {
throw new Error(result.data) throw new Error('No user with this credentials')
}
context.setHeaders.push({
key: 'token',
value: encode(result.data.user.public_hex),
}) })
const user = new User(result.data.user) if (!loginUser.emailChecked) throw new Error('user email not validated')
// Hack: Database Field is not validated properly and not nullable const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
if (user.publisherId === 0) { const loginUserPassword = BigInt(loginUser.password.toString())
user.publisherId = undefined if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
throw new Error('No user with this credentials')
} }
user.hasElopage = result.data.hasElopage // TODO: If user has no pubKey Create it again and update user.
// read additional settings from settings table
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
let userEntity: void | DbUser let userEntity: void | DbUser
userEntity = await userRepository.findByPubkeyHex(user.pubkey).catch(() => { const loginUserPubKey = loginUser.pubKey
const loginUserPubKeyString = loginUserPubKey.toString('hex')
userEntity = await userRepository.findByPubkeyHex(loginUserPubKeyString).catch(() => {
// User not stored in state_users
// TODO: Check with production data - email is unique which can cause problems
userEntity = new DbUser() userEntity = new DbUser()
userEntity.firstName = user.firstName userEntity.firstName = loginUser.firstName
userEntity.lastName = user.lastName userEntity.lastName = loginUser.lastName
userEntity.username = user.username userEntity.username = loginUser.username
userEntity.email = user.email userEntity.email = loginUser.email
userEntity.pubkey = Buffer.from(user.pubkey, 'hex') userEntity.pubkey = loginUser.pubKey
userRepository.save(userEntity).catch(() => { userRepository.save(userEntity).catch(() => {
throw new Error('error by save userEntity') throw new Error('error by save userEntity')
@ -267,16 +267,28 @@ export class UserResolver {
throw new Error('error with cannot happen') throw new Error('error with cannot happen')
} }
// Save publisherId if Elopage is not yet registered const user = new User()
user.email = email
user.firstName = loginUser.firstName
user.lastName = loginUser.lastName
user.username = loginUser.username
user.description = loginUser.description
user.pubkey = loginUserPubKeyString
user.language = loginUser.language
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ pubKey: loginUserPubKeyString })
if (!user.hasElopage && publisherId) { if (!user.hasElopage && publisherId) {
user.publisherId = publisherId user.publisherId = publisherId
// TODO: Check if we can use updateUserInfos
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
const loginUserRepository = getCustomRepository(LoginUserRepository) const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email }) const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
loginUser.publisherId = publisherId loginUser.publisherId = publisherId
loginUserRepository.save(loginUser) loginUserRepository.save(loginUser)
} }
// coinAnimation
const userSettingRepository = getCustomRepository(UserSettingRepository) const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION) .readBoolean(userEntity.id, Setting.COIN_ANIMATION)
@ -285,6 +297,12 @@ export class UserResolver {
}) })
user.coinanimation = coinanimation user.coinanimation = coinanimation
user.isAdmin = true // TODO implement user.isAdmin = true // TODO implement
context.setHeaders.push({
key: 'token',
value: encode(loginUser.pubKey),
})
return user return user
} }
@ -316,22 +334,23 @@ export class UserResolver {
@Mutation(() => String) @Mutation(() => String)
async createUser( async createUser(
@Args() { email, firstName, lastName, password, language, publisherId }: CreateUserArgs, @Args() { email, firstName, lastName, language, publisherId }: CreateUserArgs,
): Promise<string> { ): Promise<string> {
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// 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
} }
// TODO: Register process
// Validate Password // Validate Password
if (!isPassword(password)) { // if (!isPassword(password)) {
throw new Error( // throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', // 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
) // )
} // }
// Validate username // Validate username
// TODO: never true // TODO: never true
@ -349,11 +368,13 @@ export class UserResolver {
throw new Error(`User already exists.`) throw new Error(`User already exists.`)
} }
const passphrase = PassphraseGenerate() // TODO: Register process
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key // const passphrase = PassphraseGenerate()
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash // const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
const emailHash = getEmailHash(email) const emailHash = getEmailHash(email)
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
// Table: login_users // Table: login_users
const loginUser = new LoginUser() const loginUser = new LoginUser()
@ -362,13 +383,15 @@ export class UserResolver {
loginUser.lastName = lastName loginUser.lastName = lastName
loginUser.username = username loginUser.username = username
loginUser.description = '' loginUser.description = ''
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash // TODO: Register process
// loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.emailHash = emailHash loginUser.emailHash = emailHash
loginUser.language = language loginUser.language = language
loginUser.groupId = 1 loginUser.groupId = 1
loginUser.publisherId = publisherId loginUser.publisherId = publisherId
loginUser.pubKey = keyPair[0] // TODO: Register process
loginUser.privKey = encryptedPrivkey // loginUser.pubKey = keyPair[0]
// loginUser.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
@ -380,21 +403,24 @@ export class UserResolver {
throw new Error('insert user failed') throw new Error('insert user failed')
}) })
// TODO: Register process
// Table: login_user_backups // Table: login_user_backups
const loginUserBackup = new LoginUserBackup() // const loginUserBackup = new LoginUserBackup()
loginUserBackup.userId = loginUserId // loginUserBackup.userId = loginUserId
loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space // loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER; // loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
await queryRunner.manager.save(loginUserBackup).catch((error) => { // TODO: Register process
// eslint-disable-next-line no-console // await queryRunner.manager.save(loginUserBackup).catch((error) => {
console.log('insert LoginUserBackup failed', error) // // eslint-disable-next-line no-console
throw new Error('insert user backup failed') // console.log('insert LoginUserBackup failed', error)
}) // throw new Error('insert user backup failed')
// })
// Table: state_users // Table: state_users
const dbUser = new DbUser() const dbUser = new DbUser()
dbUser.pubkey = keyPair[0] // TODO: Register process
// dbUser.pubkey = keyPair[0]
dbUser.email = email dbUser.email = email
dbUser.firstName = firstName dbUser.firstName = firstName
dbUser.lastName = lastName dbUser.lastName = lastName
@ -568,7 +594,7 @@ export class UserResolver {
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
if (coinanimation) { if (coinanimation !== null && coinanimation !== undefined) {
queryRunner.manager queryRunner.manager
.getCustomRepository(UserSettingRepository) .getCustomRepository(UserSettingRepository)
.setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString()) .setOrUpdate(userEntity.id, Setting.COIN_ANIMATION, coinanimation.toString())
@ -640,7 +666,8 @@ export class UserResolver {
return false return false
} }
const elopageBuyCount = await LoginElopageBuys.count({ payerEmail: userEntity.email }) const loginElopageBuysRepository = getCustomRepository(LoginElopageBuysRepository)
const elopageBuyCount = await loginElopageBuysRepository.count({ payerEmail: userEntity.email })
return elopageBuyCount > 0 return elopageBuyCount > 0
} }
} }

View File

@ -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(),

View File

@ -0,0 +1,5 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
@EntityRepository(LoginElopageBuys)
export class LoginElopageBuysRepository extends Repository<LoginElopageBuys> {}

View File

@ -2,4 +2,23 @@ import { EntityRepository, Repository } from 'typeorm'
import { LoginUser } from '@entity/LoginUser' import { LoginUser } from '@entity/LoginUser'
@EntityRepository(LoginUser) @EntityRepository(LoginUser)
export class LoginUserRepository extends Repository<LoginUser> {} export class LoginUserRepository extends Repository<LoginUser> {
async findByEmail(email: string): Promise<LoginUser> {
return this.createQueryBuilder('loginUser')
.where('loginUser.email = :email', { email })
.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()
}
}

View File

@ -9,6 +9,15 @@ export class UserRepository extends Repository<User> {
.getOneOrFail() .getOneOrFail()
} }
async findByPubkeyHexBuffer(pubkeyHexBuffer: Buffer): Promise<User> {
const pubKeyString = pubkeyHexBuffer.toString('hex')
return await this.findByPubkeyHex(pubKeyString)
}
async findByEmail(email: string): Promise<User> {
return this.createQueryBuilder('user').where('user.email = :email', { email }).getOneOrFail()
}
async getUsersIndiced(userIds: number[]): Promise<User[]> { async getUsersIndiced(userIds: number[]): Promise<User[]> {
if (!userIds.length) return [] if (!userIds.length) return []
const users = await this.createQueryBuilder('user') const users = await this.createQueryBuilder('user')

File diff suppressed because one or more lines are too long

View File

@ -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==

View File

@ -0,0 +1,31 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('server_users')
export class ServerUser extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ length: 50 })
username: string
@Column({ type: 'bigint', unsigned: true })
password: BigInt
@Column({ length: 50, unique: true })
email: string
@Column({ length: 20, default: 'admin' })
role: string
@Column({ default: 0 })
activated: number
@Column({ name: 'last_login', default: null, nullable: true })
lastLogin: Date
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
created: Date
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
modified: Date
}

View File

@ -8,8 +8,6 @@ services:
image: gradido/frontend:development image: gradido/frontend:development
build: build:
target: development target: development
networks:
- external-net
environment: environment:
- NODE_ENV="development" - NODE_ENV="development"
# - DEBUG=true # - DEBUG=true
@ -27,8 +25,6 @@ services:
image: gradido/admin:development image: gradido/admin:development
build: build:
target: development target: development
networks:
- external-net
environment: environment:
- NODE_ENV="development" - NODE_ENV="development"
# - DEBUG=true # - DEBUG=true

View File

@ -15,6 +15,7 @@ services:
context: ./frontend context: ./frontend
target: production target: production
networks: networks:
- external-net
- internal-net - internal-net
ports: ports:
- 3000:3000 - 3000:3000
@ -39,6 +40,7 @@ services:
context: ./admin context: ./admin
target: production target: production
networks: networks:
- external-net
- internal-net - internal-net
ports: ports:
- 8080:8080 - 8080:8080

View File

@ -47,6 +47,7 @@
"change-password": "Fehler beim Ändern des Passworts", "change-password": "Fehler beim Ändern des Passworts",
"error": "Fehler", "error": "Fehler",
"no-account": "Leider konnten wir keinen Account finden mit diesen Daten!", "no-account": "Leider konnten wir keinen Account finden mit diesen Daten!",
"no-email-verify": "Die Email wurde noch nicht bestätigt, bitte überprüfe deine Emails und klicke auf den Aktivierungslink!",
"session-expired": "Sitzung abgelaufen!" "session-expired": "Sitzung abgelaufen!"
}, },
"form": { "form": {
@ -181,9 +182,12 @@
"uppercase": "Ein Großbuchstabe erforderlich." "uppercase": "Ein Großbuchstabe erforderlich."
}, },
"thx": { "thx": {
"activateEmail": "Deine Email wurde noch nicht aktiviert, bitte überprüfe deine Email und Klicke den Aktivierungslink!",
"checkEmail": "Deine Email würde erfolgreich verifiziert.", "checkEmail": "Deine Email würde erfolgreich verifiziert.",
"email": "Wir haben dir eine eMail gesendet.", "email": "Wir haben dir eine eMail gesendet.",
"register": "Du bist jetzt registriert.", "emailActivated": "Danke dass Du deine Email bestätigt hast.",
"errorTitle": "Achtung!",
"register": "Du bist jetzt registriert, bitte überprüfe deine Emails und klicke auf den Aktivierungslink.",
"reset": "Dein Passwort wurde geändert.", "reset": "Dein Passwort wurde geändert.",
"title": "Danke!" "title": "Danke!"
} }

View File

@ -47,6 +47,7 @@
"change-password": "Error while changing password", "change-password": "Error while changing password",
"error": "Error", "error": "Error",
"no-account": "Unfortunately we could not find an account to the given data!", "no-account": "Unfortunately we could not find an account to the given data!",
"no-email-verify": "Your email is not activated yet, please check your emails and click the activation link!",
"session-expired": "The session expired" "session-expired": "The session expired"
}, },
"form": { "form": {
@ -181,9 +182,12 @@
"uppercase": "One uppercase letter required." "uppercase": "One uppercase letter required."
}, },
"thx": { "thx": {
"activateEmail": "Your email has not been activated yet, please check your emails and click the activation link!",
"checkEmail": "Your email has been successfully verified.", "checkEmail": "Your email has been successfully verified.",
"email": "We have sent you an email.", "email": "We have sent you an email.",
"register": "You are registred now.", "emailActivated": "Thank you your email has been activated.",
"errorTitle": "Attention!",
"register": "You are registered now, please check your emails and click the activation link.",
"reset": "Your password has been changed.", "reset": "Your password has been changed.",
"title": "Thank you!" "title": "Thank you!"
} }

View File

@ -50,7 +50,7 @@ const routes = [
path: '/thx/:comingFrom', path: '/thx/:comingFrom',
component: () => import('../views/Pages/thx.vue'), component: () => import('../views/Pages/thx.vue'),
beforeEnter: (to, from, next) => { beforeEnter: (to, from, next) => {
const validFrom = ['password', 'reset', 'register'] const validFrom = ['password', 'reset', 'register', 'login']
if (!validFrom.includes(from.path.split('/')[1])) { if (!validFrom.includes(from.path.split('/')[1])) {
next({ path: '/login' }) next({ path: '/login' })
} else { } else {

View File

@ -104,9 +104,14 @@ export default {
this.$router.push('/overview') this.$router.push('/overview')
loader.hide() loader.hide()
}) })
.catch(() => { .catch((error) => {
if (!error.message.includes('user email not validated')) {
this.$toasted.error(this.$t('error.no-account'))
} else {
// : this.$t('error.no-email-verify')
this.$router.push('/thx/login')
}
loader.hide() loader.hide()
this.$toasted.error(this.$t('error.no-account'))
}) })
}, },
}, },

View File

@ -161,6 +161,7 @@ import InputEmail from '../../components/Inputs/InputEmail.vue'
import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation.vue' import InputPasswordConfirmation from '../../components/Inputs/InputPasswordConfirmation.vue'
import LanguageSwitchSelect from '../../components/LanguageSwitchSelect.vue' import LanguageSwitchSelect from '../../components/LanguageSwitchSelect.vue'
import { registerUser } from '../../graphql/mutations' import { registerUser } from '../../graphql/mutations'
import { localeChanged } from 'vee-validate'
import { getCommunityInfoMixin } from '../../mixins/getCommunityInfo' import { getCommunityInfoMixin } from '../../mixins/getCommunityInfo'
export default { export default {
@ -189,6 +190,9 @@ export default {
methods: { methods: {
updateLanguage(e) { updateLanguage(e) {
this.language = e this.language = e
this.$store.commit('language', this.language)
this.$i18n.locale = this.language
localeChanged(this.language)
}, },
getValidationState({ dirty, validated, valid = null }) { getValidationState({ dirty, validated, valid = null }) {
return dirty || validated ? valid : null return dirty || validated ? valid : null

View File

@ -4,10 +4,12 @@
<div class="header py-7 py-lg-8 pt-lg-9"> <div class="header py-7 py-lg-8 pt-lg-9">
<b-container> <b-container>
<div class="header-body text-center mb-7"> <div class="header-body text-center mb-7">
<p class="h1">{{ $t('site.thx.title') }}</p> <p class="h1">{{ $t(displaySetup.headline) }}</p>
<p class="h4">{{ $t(displaySetup.subtitle) }}</p> <p class="h4">{{ $t(displaySetup.subtitle) }}</p>
<hr /> <hr />
<b-button :to="displaySetup.linkTo">{{ $t(displaySetup.button) }}</b-button> <b-button v-if="displaySetup.linkTo" :to="displaySetup.linkTo">
{{ $t(displaySetup.button) }}
</b-button>
</div> </div>
</b-container> </b-container>
</div> </div>
@ -17,25 +19,33 @@
<script> <script>
const textFields = { const textFields = {
password: { password: {
headline: 'site.thx.title',
subtitle: 'site.thx.email', subtitle: 'site.thx.email',
button: 'login', button: 'login',
linkTo: '/login', linkTo: '/login',
}, },
reset: { reset: {
headline: 'site.thx.title',
subtitle: 'site.thx.reset', subtitle: 'site.thx.reset',
button: 'login', button: 'login',
linkTo: '/login', linkTo: '/login',
}, },
register: { register: {
headline: 'site.thx.title',
subtitle: 'site.thx.register', subtitle: 'site.thx.register',
button: 'site.login.signin', button: 'site.login.signin',
linkTo: '/overview', linkTo: '/overview',
}, },
checkEmail: { checkEmail: {
headline: 'site.thx.title',
subtitle: 'site.thx.checkEmail', subtitle: 'site.thx.checkEmail',
button: 'login', button: 'login',
linkTo: '/login', linkTo: '/login',
}, },
login: {
headline: 'site.thx.errorTitle',
subtitle: 'site.thx.activateEmail',
},
} }
export default { export default {

View File

@ -147,7 +147,6 @@ Poco::JSON::Object* JsonUnsecureLogin::handle(Poco::Dynamic::Var params)
infos.add("set user.group_id to default group_id = 1"); infos.add("set user.group_id to default group_id = 1");
case USER_NO_PRIVATE_KEY: case USER_NO_PRIVATE_KEY:
case USER_COMPLETE: case USER_COMPLETE:
case USER_EMAIL_NOT_ACTIVATED:
result->set("state", "success"); result->set("state", "success");
result->set("user", session->getNewUser()->getJson()); result->set("user", session->getNewUser()->getJson());
result->set("session_id", session->getHandle()); result->set("session_id", session->getHandle());
@ -158,6 +157,10 @@ Poco::JSON::Object* JsonUnsecureLogin::handle(Poco::Dynamic::Var params)
AWAIT(hasElopageTask) AWAIT(hasElopageTask)
result->set("hasElopage", hasElopageTask->hasElopage()); result->set("hasElopage", hasElopageTask->hasElopage());
return result; return result;
case USER_EMAIL_NOT_ACTIVATED:
result->set("state", "processing");
result->set("msg", "user email not validated");
break;
default: default:
result->set("state", "error"); result->set("state", "error");
result->set("msg", "unknown user state"); result->set("msg", "unknown user state");