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

View File

@ -1,6 +1,11 @@
module.exports = {
verbose: true,
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!**/node_modules/**',
'!src/assets/**',
'!**/?(*.)+(spec|test).js?(x)',
],
moduleFileExtensions: [
'js',
// 'jsx',

View File

@ -35,6 +35,7 @@
"identity-obj-proxy": "^3.0.0",
"jest": "26.6.3",
"moment": "^2.29.1",
"regenerator-runtime": "^0.13.9",
"stats-webpack-plugin": "^0.7.0",
"vue": "^2.6.11",
"vue-apollo": "^3.0.8",

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>
</template>
<script>
export default {
name: 'ContentFooter',
}
</script>

View File

@ -19,7 +19,7 @@ const mocks = {
const propsData = {
type: '',
item: {},
creation: {},
creation: [],
itemsMassCreation: {},
}
@ -38,5 +38,104 @@ describe('CreationFormular', () => {
it('has a DIV element with the class.component-creation-formular', () => {
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
})
describe('radio buttons to selcet month', () => {
it('has three radio buttons', () => {
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
})
describe('with mass creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'massCreation' })
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('emits update-radio-selected with index 0', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([0])],
])
})
})
describe('second radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('emits update-radio-selected with index 1', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([1])],
])
})
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('emits update-radio-selected with index 2', () => {
expect(wrapper.emitted()['update-radio-selected']).toEqual([
[expect.arrayContaining([2])],
])
})
})
})
describe('with single creation', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
await wrapper.setData({ rangeMin: 180 })
})
describe('first radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 200', () => {
expect(wrapper.vm.rangeMax).toBe(200)
})
})
describe('second radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 400', () => {
expect(wrapper.vm.rangeMax).toBe(400)
})
})
describe('third radio button', () => {
beforeEach(async () => {
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
})
it('sets rangeMin to 0', () => {
expect(wrapper.vm.rangeMin).toBe(0)
})
it('sets rangeMax to 400', () => {
expect(wrapper.vm.rangeMax).toBe(600)
})
})
})
})
})
})

View File

@ -4,9 +4,10 @@
<h3>
{{
this.type === 'singleCreation'
? 'Einzelschöpfung für ' + item.first_name + ' ' + item.last_name + ''
: 'Massenschöpfung für ' + Object.keys(this.itemsMassCreation).length + ' Mitglieder'
? '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
@ -26,7 +27,7 @@
size="lg"
@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-col>
<b-col>
@ -36,7 +37,7 @@
size="lg"
@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-col>
<b-col class="text-right">
@ -46,7 +47,7 @@
size="lg"
@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-col>
</b-row>
@ -73,6 +74,7 @@
:min="rangeMin"
:max="rangeMax"
step="10"
@load="checkFormForUpdate('range')"
></b-input>
</b-row>
<b-row class="m-4">
@ -83,6 +85,7 @@
v-model="text"
:state="text.length >= 10"
placeholder="Mindestens 10 Zeichen eingeben"
@load="checkFormForUpdate('text')"
rows="3"
></b-form-textarea>
</div>
@ -96,6 +99,17 @@
<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"
@ -116,14 +130,23 @@ export default {
props: {
type: {
type: String,
required: true,
required: false,
},
pagetype: {
type: String,
required: false,
default: '',
},
item: {
type: Object,
required: false,
},
creation: {
creationUserData: {
type: Object,
required: false,
},
creation: {
type: Array,
required: true,
},
itemsMassCreation: {
@ -138,9 +161,18 @@ export default {
value: 0,
rangeMin: 0,
rangeMax: 1000,
currentMonth: this.$moment().format('MMMM'),
lastMonth: this.$moment().subtract(1, 'month').format('MMMM'),
beforeLastMonth: this.$moment().subtract(2, 'month').format('MMMM'),
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,
}
@ -148,7 +180,7 @@ export default {
methods: {
// Auswählen eines Zeitraumes
updateRadioSelected(name, index, openCreation) {
// Wenn Massenschöpfung
// Wenn Mehrfachschöpfung
if (this.type === 'massCreation') {
// An Creation.vue emitten und radioSelectedMass aktualisieren
this.$emit('update-radio-selected', [name, index])
@ -160,6 +192,19 @@ export default {
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 === '') {
@ -178,9 +223,9 @@ export default {
return alert('Bitte gib einen Text ein der länger als 10 Zeichen ist!')
}
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
// 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')
this.submitObj = [
{
@ -191,32 +236,44 @@ export default {
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
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')
}
if (this.type === 'singleCreation') {
// 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
this.submitObj = [
{
item: this.item,
datum: this.radioSelected,
datum: this.radioSelected.long,
amount: this.value,
text: this.text,
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 + '')
// $store - offene Schöpfungen hochzählen
this.$store.commit('openCreationsPlus', 1)
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

View File

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

View File

@ -8,7 +8,7 @@
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<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
v-show="$store.state.openCreations > 0"
class="h5 bg-danger"

View File

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

View File

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

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 App from './App.vue'
// without this async calls are not working
import 'regenerator-runtime'
import store from './store/store'
import router from './router/router'
@ -22,7 +25,8 @@ import moment from 'vue-moment'
const httpLink = new HttpLink({ uri: CONFIG.GRAPHQL_URI })
const authLink = new ApolloLink((operation, forward) => {
const token = '' // store.state.token
const token = store.state.token
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
@ -52,10 +56,13 @@ const apolloProvider = new VueApollo({
})
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)
Vue.use(moment)
Vue.use(VueApollo)
addNavigationGuards(router, store)
new Vue({

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="creation">
<b-row>
<b-col cols="12" lg="5">
<label>Usersuche</label>
@ -10,6 +10,7 @@
placeholder="User suche"
></b-input>
<user-table
v-if="itemsList.length > 0"
type="UserListSearch"
:itemsUser="itemsList"
:fieldsTable="Searchfields"
@ -20,7 +21,7 @@
</b-col>
<b-col cols="12" lg="7" class="shadow p-3 mb-5 rounded bg-info">
<user-table
v-show="Object.keys(this.massCreation).length > 0"
v-if="massCreation.length > 0"
class="shadow p-3 mb-5 bg-white rounded"
type="UserListMassCreation"
:itemsUser="massCreation"
@ -31,6 +32,7 @@
/>
<creation-formular
v-if="massCreation.length > 0"
type="massCreation"
:creation="creation"
:itemsMassCreation="massCreation"
@ -44,9 +46,10 @@
<script>
import CreationFormular from '../components/CreationFormular.vue'
import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
export default {
name: 'overview',
name: 'Creation',
components: {
CreationFormular,
UserTable,
@ -56,60 +59,49 @@ export default {
showArrays: false,
Searchfields: [
{ key: 'bookmark', label: 'merken' },
{ key: 'first_name', label: 'Firstname' },
{ key: 'last_name', label: 'Lastname' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' },
{ key: 'email', label: 'Email' },
],
fields: [
{ key: 'email', label: 'Email' },
{ key: 'first_name', label: 'Firstname' },
{ key: 'last_name', label: 'Lastname' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' },
{ key: 'bookmark', label: 'löschen' },
],
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',
},
],
itemsList: this.searchResult,
itemsList: [],
massCreation: [],
radioSelectedMass: '',
criteria: '',
creation: [null, null, null],
}
},
created() {
this.itemsList = this.searchResult
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 = {}

View File

@ -1,5 +1,5 @@
<template>
<div>
<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!
@ -8,9 +8,8 @@
class="mt-4"
type="PageCreationConfirm"
:itemsUser="confirmResult"
:creation="creation"
:fieldsTable="fields"
@update-confirm-result="updateConfirmResult"
@remove-confirm-result="removeConfirmResult"
/>
</div>
</template>
@ -18,7 +17,7 @@
import UserTable from '../components/UserTable.vue'
export default {
name: 'creation_confirm',
name: 'CreationConfirm',
components: {
UserTable,
},
@ -28,11 +27,23 @@ export default {
fields: [
{ key: 'bookmark', label: 'löschen' },
{ key: 'email', label: 'Email' },
{ key: 'first_name', label: 'Vorname' },
{ key: 'last_name', label: 'Nachname' },
{ key: 'creation_gdd', label: 'GDD' },
{ 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' },
{
key: 'creation_date',
label: 'Datum',
formatter: (value) => {
return value.long
},
},
{ key: 'creation_moderator', label: 'Moderator' },
{ key: 'edit_creation', label: 'ändern' },
{ key: 'confirm', label: 'speichern' },
@ -41,67 +52,81 @@ export default {
{
id: 1,
email: 'dickerson@web.de',
first_name: 'Dickerson',
last_name: 'Macdonald',
creation: '450,200,700',
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: '01/11/2021',
creation_date: {
short: 'November',
long: '22/11/2021',
},
creation_moderator: 'Manuela Gast',
},
{
id: 2,
email: 'larsen@woob.de',
first_name: 'Larsen',
last_name: 'Shaw',
creation: '300,200,1000',
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: '01/11/2021',
creation_date: {
short: 'November',
long: '03/11/2021',
},
creation_moderator: 'Manuela Gast',
},
{
id: 3,
email: 'geneva@tete.de',
first_name: 'Geneva',
last_name: 'Wilson',
creation: '350,200,900',
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: '01/11/2021',
creation_date: {
short: 'September',
long: '27/09/2021',
},
creation_moderator: 'Manuela Gast',
},
{
id: 4,
email: 'viewrter@asdfvb.com',
first_name: 'Soledare',
last_name: 'Takker',
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: '01/10/2021',
creation_date: {
short: 'Oktober',
long: '12/10/2021',
},
creation_moderator: 'Evelyn Roller',
},
{
id: 5,
email: 'dickerson@web.de',
first_name: 'Dickerson',
last_name: 'Macdonald',
creation: '100,400,800',
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: '01/09/2021',
creation_date: {
short: 'September',
long: '05/09/2021',
},
creation_moderator: 'Manuela Gast',
},
],
creation: [null, null, null],
}
},
methods: {
updateConfirmResult(e, event) {
removeConfirmResult(e, event) {
if (event === 'remove') {
let index = 0
let findArr = {}

View File

@ -1,11 +1,12 @@
<template>
<div>
<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"
@ -17,9 +18,10 @@
</template>
<script>
import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers'
export default {
name: 'overview',
name: 'UserSearch',
components: {
UserTable,
},
@ -28,46 +30,41 @@ export default {
showArrays: false,
fields: [
{ key: 'email', label: 'Email' },
{ key: 'first_name', label: 'Firstname' },
{ key: 'last_name', label: 'Lastname' },
{ key: 'firstName', label: 'Firstname' },
{ key: 'lastName', label: 'Lastname' },
{ key: 'creation', label: 'Creation' },
{ key: 'show_details', label: 'Details' },
],
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',
},
],
searchResult: [],
massCreation: [],
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>

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 = {
openCreationsPlus: (state, i) => {
state.openCreations = state.openCreations + i
state.openCreations += i
},
openCreationsMinus: (state, i) => {
state.openCreations = state.openCreations - i
state.openCreations -= i
},
resetOpenCreations: (state) => {
state.openCreations = 0

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 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.use(BootstrapVue)
global.localVue.use(IconsPlugin)
// throw errors for vue warnings to force the programmers to take care about warnings
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"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.13.4:
regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9:
version "0.13.9"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==

View File

@ -30,4 +30,6 @@ COMMUNITY_URL=
COMMUNITY_REGISTER_URL=
COMMUNITY_DESCRIPTION=
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-testing": "^2.25.2",
"axios": "^0.21.1",
"body-parser": "^1.19.0",
"class-validator": "^0.13.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",

View File

@ -51,14 +51,25 @@ const email = {
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'gmail.com',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587',
EMAIL_LINK_VERIFICATION:
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1',
}
const webhook = {
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
}
// This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET
const CONFIG = { ...server, ...database, ...klicktipp, ...community, ...email, ...loginServer }
const CONFIG = {
...server,
...database,
...klicktipp,
...community,
...email,
...loginServer,
...webhook,
}
export default CONFIG

View File

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

View File

@ -2,9 +2,6 @@
import { AuthChecker } from 'type-graphql'
import CONFIG from '../../config'
import { apiGet } from '../../apis/HttpRequest'
import decode from '../../jwt/decode'
import encode from '../../jwt/encode'
@ -13,7 +10,7 @@ const isAuthorized: AuthChecker<any> = async (
) => {
if (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) })
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()
} catch (e) {
await queryRunner.rollbackTransaction()
throw e
} finally {
await queryRunner.release()
// 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
// index for the transaction which requires a chain without gaps
@ -627,6 +624,9 @@ export class TransactionResolver {
// eslint-disable-next-line no-console
console.log('problems with reset auto increment: %o', error)
})
throw e
} finally {
await queryRunner.release()
}
// send notification email
// TODO: translate

View File

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

View File

@ -6,6 +6,7 @@ import 'module-alias/register'
import { ApolloServer } from 'apollo-server-express'
import express from 'express'
import bodyParser from 'body-parser'
// database
import connection from '../typeorm/connection'
@ -22,6 +23,9 @@ import CONFIG from '../config'
// graphql
import schema from '../graphql/schema'
// webhooks
import { elopageWebhook } from '../webhook/elopage'
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
@ -50,6 +54,12 @@ const createServer = async (context: any = serverContext): Promise<any> => {
// cors
app.use(cors)
// bodyparser
app.use(bodyParser.json())
// Elopage Webhook
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
// Apollo Server
const apollo = new ApolloServer({
schema: await schema(),

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'
@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()
}
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[]> {
if (!userIds.length) return []
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"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
body-parser@1.19.0, body-parser@^1.18.3:
body-parser@1.19.0, body-parser@^1.18.3, body-parser@^1.19.0:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==

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
build:
target: development
networks:
- external-net
environment:
- NODE_ENV="development"
# - DEBUG=true
@ -27,8 +25,6 @@ services:
image: gradido/admin:development
build:
target: development
networks:
- external-net
environment:
- NODE_ENV="development"
# - DEBUG=true

View File

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

View File

@ -47,6 +47,7 @@
"change-password": "Fehler beim Ändern des Passworts",
"error": "Fehler",
"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!"
},
"form": {
@ -181,9 +182,12 @@
"uppercase": "Ein Großbuchstabe erforderlich."
},
"thx": {
"activateEmail": "Deine Email wurde noch nicht aktiviert, bitte überprüfe deine Email und Klicke den Aktivierungslink!",
"checkEmail": "Deine Email würde erfolgreich verifiziert.",
"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.",
"title": "Danke!"
}

View File

@ -47,6 +47,7 @@
"change-password": "Error while changing password",
"error": "Error",
"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"
},
"form": {
@ -181,9 +182,12 @@
"uppercase": "One uppercase letter required."
},
"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.",
"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.",
"title": "Thank you!"
}

View File

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

View File

@ -104,9 +104,14 @@ export default {
this.$router.push('/overview')
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()
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 LanguageSwitchSelect from '../../components/LanguageSwitchSelect.vue'
import { registerUser } from '../../graphql/mutations'
import { localeChanged } from 'vee-validate'
import { getCommunityInfoMixin } from '../../mixins/getCommunityInfo'
export default {
@ -189,6 +190,9 @@ export default {
methods: {
updateLanguage(e) {
this.language = e
this.$store.commit('language', this.language)
this.$i18n.locale = this.language
localeChanged(this.language)
},
getValidationState({ 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">
<b-container>
<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>
<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>
</b-container>
</div>
@ -17,25 +19,33 @@
<script>
const textFields = {
password: {
headline: 'site.thx.title',
subtitle: 'site.thx.email',
button: 'login',
linkTo: '/login',
},
reset: {
headline: 'site.thx.title',
subtitle: 'site.thx.reset',
button: 'login',
linkTo: '/login',
},
register: {
headline: 'site.thx.title',
subtitle: 'site.thx.register',
button: 'site.login.signin',
linkTo: '/overview',
},
checkEmail: {
headline: 'site.thx.title',
subtitle: 'site.thx.checkEmail',
button: 'login',
linkTo: '/login',
},
login: {
headline: 'site.thx.errorTitle',
subtitle: 'site.thx.activateEmail',
},
}
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");
case USER_NO_PRIVATE_KEY:
case USER_COMPLETE:
case USER_EMAIL_NOT_ACTIVATED:
result->set("state", "success");
result->set("user", session->getNewUser()->getJson());
result->set("session_id", session->getHandle());
@ -158,6 +157,10 @@ Poco::JSON::Object* JsonUnsecureLogin::handle(Poco::Dynamic::Var params)
AWAIT(hasElopageTask)
result->set("hasElopage", hasElopageTask->hasElopage());
return result;
case USER_EMAIL_NOT_ACTIVATED:
result->set("state", "processing");
result->set("msg", "user email not validated");
break;
default:
result->set("state", "error");
result->set("msg", "unknown user state");