Merge branch 'master' into refactor_remove_community_server

This commit is contained in:
Alexander Friedland 2022-02-07 10:50:19 +01:00 committed by GitHub
commit a7544a5151
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1182 additions and 622 deletions

View File

@ -422,7 +422,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: 81 min_coverage: 93
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -13,3 +13,11 @@ export default {
components: { defaultLayout }, components: { defaultLayout },
} }
</script> </script>
<style>
.pointer {
cursor: pointer;
}
.pointer:hover {
background-color: rgb(216, 213, 213);
}
</style>

View File

@ -11,22 +11,36 @@ describe('UserTable', () => {
const defaultItemsUser = [ const defaultItemsUser = [
{ {
email: 'bibi@bloxberg.de', userId: 1,
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
creation: [1000, 1000, 1000], email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
}, },
{ {
email: 'bibi@bloxberg.de', userId: 2,
firstName: 'Bibi', firstName: 'Benjamin',
lastName: 'Bloxberg', lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000], creation: [1000, 1000, 1000],
emailChecked: true,
}, },
{ {
email: 'bibi@bloxberg.de', userId: 3,
firstName: 'Bibi', firstName: 'Peter',
lastName: 'Bloxberg', lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
},
{
userId: 4,
firstName: 'New',
lastName: 'User',
email: 'new@user.ch',
creation: [1000, 1000, 1000], creation: [1000, 1000, 1000],
emailChecked: false,
}, },
] ]
@ -107,7 +121,7 @@ describe('UserTable', () => {
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d), $d: jest.fn((d) => String(d)),
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
}, },
@ -122,7 +136,7 @@ describe('UserTable', () => {
describe('mount', () => { describe('mount', () => {
describe('type PageUserSearch', () => { describe('type PageUserSearch', () => {
beforeEach(() => { beforeEach(async () => {
wrapper = Wrapper(propsDataPageUserSearch) wrapper = Wrapper(propsDataPageUserSearch)
}) })
@ -175,12 +189,12 @@ describe('UserTable', () => {
}) })
describe('content', () => { describe('content', () => {
it('has 3 rows', () => { it('has 4 rows', () => {
expect(wrapper.findAll('tbody tr').length).toBe(3) expect(wrapper.findAll('tbody tr')).toHaveLength(4)
}) })
it('has 7 columns', () => { it('has 7 columns', () => {
expect(wrapper.findAll('tr:nth-child(1) > td').length).toBe(7) expect(wrapper.findAll('tr:nth-child(1) > td')).toHaveLength(7)
}) })
it('find button on fifth column', () => { it('find button on fifth column', () => {
@ -189,6 +203,110 @@ describe('UserTable', () => {
).toBeTruthy() ).toBeTruthy()
}) })
}) })
describe('row toggling', () => {
describe('user with email not activated', () => {
it('has no details button', () => {
expect(
wrapper.findAll('tbody > tr').at(3).findAll('td').at(4).find('button').exists(),
).toBeFalsy()
})
it('has a red confirmed button with envelope item', () => {
const row = wrapper.findAll('tbody > tr').at(3)
expect(row.findAll('td').at(5).find('button').exists()).toBeTruthy()
expect(row.findAll('td').at(5).find('button').classes('btn-danger')).toBeTruthy()
expect(row.findAll('td').at(5).find('svg').classes('bi-envelope')).toBeTruthy()
})
describe('click on envelope', () => {
beforeEach(async () => {
await wrapper
.findAll('tbody > tr')
.at(3)
.findAll('td')
.at(5)
.find('button')
.trigger('click')
})
it('opens the details', async () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(6)
expect(wrapper.findAll('tbody > tr').at(5).find('input').element.value).toBe(
'new@user.ch',
)
expect(wrapper.findAll('tbody > tr').at(5).text()).toContain(
'unregister_mail.text_false',
)
// HACK: for some reason we need to close the row details after this test
await wrapper
.findAll('tbody > tr')
.at(3)
.findAll('td')
.at(5)
.find('button')
.trigger('click')
})
describe('click on envelope again', () => {
beforeEach(async () => {
await wrapper
.findAll('tbody > tr')
.at(3)
.findAll('td')
.at(5)
.find('button')
.trigger('click')
})
it('closes the details', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
})
})
describe('click on close details', () => {
beforeEach(async () => {
await wrapper.findAll('tbody > tr').at(5).findAll('button').at(1).trigger('click')
})
it('closes the details', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
})
})
})
})
describe('different details', () => {
it.skip('shows the creation formular for second user', async () => {
await wrapper
.findAll('tbody > tr')
.at(1)
.findAll('td')
.at(4)
.find('button')
.trigger('click')
expect(wrapper.findAll('tbody > tr')).toHaveLength(6)
expect(
wrapper
.findAll('tbody > tr')
.at(3)
.find('div.component-creation-formular')
.exists(),
).toBeTruthy()
})
it.skip('shows the transactions for third user', async () => {
await wrapper
.findAll('tbody > tr')
.at(4)
.findAll('td')
.at(6)
.find('button')
.trigger('click')
expect(wrapper.findAll('tbody > tr')).toHaveLength(6)
})
})
})
}) })
}) })

View File

@ -27,15 +27,7 @@
</b-button> </b-button>
</b-jumbotron> </b-jumbotron>
</div> </div>
<b-table-lite <b-table-lite :items="itemsUser" :fields="fieldsTable" caption-top striped hover stacked="md">
:items="itemsUser"
:fields="fieldsTable"
:filter="criteria"
caption-top
striped
hover
stacked="md"
>
<template #cell(creation)="data"> <template #cell(creation)="data">
<div v-html="data.value"></div> <div v-html="data.value"></div>
</template> </template>
@ -125,7 +117,7 @@
</row-details> </row-details>
</template> </template>
<template #cell(bookmark)="row"> <template #cell(bookmark)="row">
<div v-show="type === 'UserListSearch'"> <div v-if="type === 'UserListSearch'">
<b-button <b-button
v-if="row.item.emailChecked" v-if="row.item.emailChecked"
variant="warning" variant="warning"
@ -141,7 +133,7 @@
variant="danger" variant="danger"
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'" v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
size="md" size="md"
@click="overlayShow('remove', row.item)" @click="bookmarkRemove(row.item)"
class="mr-2" class="mr-2"
> >
<b-icon icon="x" variant="light"></b-icon> <b-icon icon="x" variant="light"></b-icon>
@ -187,15 +179,6 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
criteria: {
type: String,
required: false,
default: '',
},
creation: {
type: Array,
required: false,
},
}, },
components: { components: {
CreationFormular, CreationFormular,
@ -259,13 +242,6 @@ export default {
this.overlayBookmarkType = bookmarkType this.overlayBookmarkType = bookmarkType
this.overlayItem = item this.overlayItem = item
if (bookmarkType === 'remove') {
this.overlayText.header = this.$t('overlay.remove.title')
this.overlayText.text1 = this.$t('overlay.remove.text')
this.overlayText.text2 = this.$t('overlay.remove.question')
this.overlayText.button_ok = this.$t('overlay.remove.yes')
this.overlayText.button_cancel = this.$t('overlay.remove.no')
}
if (bookmarkType === 'confirm') { if (bookmarkType === 'confirm') {
this.overlayText.header = this.$t('overlay.confirm.title') this.overlayText.header = this.$t('overlay.confirm.title')
this.overlayText.text1 = this.$t('overlay.confirm.text') this.overlayText.text1 = this.$t('overlay.confirm.text')
@ -275,9 +251,6 @@ export default {
} }
}, },
overlayOK(bookmarkType, item) { overlayOK(bookmarkType, item) {
if (bookmarkType === 'remove') {
this.bookmarkRemove(item)
}
if (bookmarkType === 'confirm') { if (bookmarkType === 'confirm') {
this.$emit('confirm-creation', item) this.$emit('confirm-creation', item)
} }

View File

@ -54,6 +54,7 @@
} }
}, },
"remove": "Entfernen", "remove": "Entfernen",
"remove_all": "alle Nutzer entfernen",
"transaction": "Transaktion", "transaction": "Transaktion",
"transactionlist": { "transactionlist": {
"amount": "Betrag", "amount": "Betrag",

View File

@ -54,6 +54,7 @@
} }
}, },
"remove": "Remove", "remove": "Remove",
"remove_all": "Remove all users",
"transaction": "Transaction", "transaction": "Transaction",
"transactionlist": { "transactionlist": {
"amount": "Amount", "amount": "Amount",

View File

@ -1,6 +1,9 @@
export const creationMonths = { export const creationMonths = {
props: { props: {
creation: [1000, 1000, 1000], creation: {
type: Array,
default: () => [1000, 1000, 1000],
},
}, },
computed: { computed: {
creationDates() { creationDates() {
@ -31,5 +34,8 @@ export const creationMonths = {
} }
}) })
}, },
creationLabel() {
return this.creationDates.map((date) => this.$d(date, 'monthShort')).join(' | ')
},
}, },
} }

View File

@ -1,4 +1,4 @@
import { shallowMount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Creation from './Creation.vue' import Creation from './Creation.vue'
const localVue = global.localVue const localVue = global.localVue
@ -14,6 +14,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Bloxberg', lastName: 'Bloxberg',
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
creation: [200, 400, 600], creation: [200, 400, 600],
emailChecked: true,
}, },
{ {
userId: 2, userId: 2,
@ -21,6 +22,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Blümchen', lastName: 'Blümchen',
email: 'benjamin@bluemchen.de', email: 'benjamin@bluemchen.de',
creation: [800, 600, 400], creation: [800, 600, 400],
emailChecked: true,
}, },
], ],
}, },
@ -51,10 +53,10 @@ describe('Creation', () => {
let wrapper let wrapper
const Wrapper = () => { const Wrapper = () => {
return shallowMount(Creation, { localVue, mocks }) return mount(Creation, { localVue, mocks })
} }
describe('shallowMount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
wrapper = Wrapper() wrapper = Wrapper()
@ -77,64 +79,66 @@ describe('Creation', () => {
) )
}) })
it('sets the data of itemsList', () => { it('has two rows in the left table', () => {
expect(wrapper.vm.itemsList).toEqual([ expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
{ })
userId: 1,
firstName: 'Bibi', it('has nwo rows in the right table', () => {
lastName: 'Bloxberg', expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
email: 'bibi@bloxberg.de', })
creation: [200, 400, 600],
showDetails: false, it('has correct data in first row ', () => {
}, expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi')
{ expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
userId: 2, 'Bloxberg',
firstName: 'Benjamin', )
lastName: 'Blümchen', expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
email: 'benjamin@bluemchen.de', '200 | 400 | 600',
creation: [800, 600, 400], )
showDetails: false, expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
}, 'bibi@bloxberg.de',
]) )
})
it('has correct data in second row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Benjamin',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Blümchen',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'800 | 600 | 400',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'benjamin@bluemchen.de',
)
}) })
}) })
describe('push item', () => { describe('push item', () => {
beforeEach(() => { beforeEach(() => {
wrapper.findComponent({ name: 'UserTable' }).vm.$emit('push-item', { wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click')
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
})
}) })
it('removes the pushed item from itemsList', () => { it('has one item in left table', () => {
expect(wrapper.vm.itemsList).toEqual([ expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
showDetails: false,
},
])
}) })
it('adds the pushed item to itemsMassCreation', () => { it('has one item in right table', () => {
expect(wrapper.vm.itemsMassCreation).toEqual([ expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
{ })
userId: 2,
firstName: 'Benjamin', it('has the correct user in left table', () => {
lastName: 'Blümchen', expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
email: 'benjamin@bluemchen.de', 'bibi@bloxberg.de',
creation: [800, 600, 400], )
showDetails: false, })
},
]) it('has the correct user in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
}) })
it('updates userSelectedInMassCreation in store', () => { it('updates userSelectedInMassCreation in store', () => {
@ -146,88 +150,58 @@ describe('Creation', () => {
email: 'benjamin@bluemchen.de', email: 'benjamin@bluemchen.de',
creation: [800, 600, 400], creation: [800, 600, 400],
showDetails: false, showDetails: false,
}, emailChecked: true,
])
})
})
describe('remove item', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit('push-item', {
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
})
await wrapper
.findAllComponents({ name: 'UserTable' })
.at(1)
.vm.$emit('remove-item', {
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
})
})
it('adds the removed item to itemsList', () => {
expect(wrapper.vm.itemsList).toEqual([
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
},
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
showDetails: false,
}, },
]) ])
}) })
it('removes the item from itemsMassCreation', () => { describe('remove item', () => {
expect(wrapper.vm.itemsMassCreation).toEqual([]) beforeEach(async () => {
}) await wrapper
.findAll('table')
it('commits empty array as userSelectedInMassCreation', () => { .at(1)
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', []) .findAll('tbody > tr')
}) .at(0)
}) .find('button')
.trigger('click')
describe('remove all bookmarks', () => { })
beforeEach(async () => {
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit('push-item', { it('has two items in left table', () => {
userId: 2, expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
firstName: 'Benjamin', })
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de', it('has the removed user in first row', () => {
creation: [800, 600, 400], expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
showDetails: false, 'benjamin@bluemchen.de',
)
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array as userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
}) })
jest.clearAllMocks()
wrapper.findComponent({ name: 'CreationFormular' }).vm.$emit('remove-all-bookmark')
}) })
it('removes all items from itemsMassCreation', () => { describe('remove all bookmarks', () => {
expect(wrapper.vm.itemsMassCreation).toEqual([]) beforeEach(async () => {
}) jest.clearAllMocks()
await wrapper.find('button.btn-light').trigger('click')
})
it('commits empty array to userSelectedInMassCreation', () => { it('has no items in right table', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', []) expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
}) })
it('calls searchUsers', () => { it('commits empty array to userSelectedInMassCreation', () => {
expect(apolloQueryMock).toBeCalled() expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
it('calls searchUsers', () => {
expect(apolloQueryMock).toBeCalled()
})
}) })
}) })
@ -241,22 +215,24 @@ describe('Creation', () => {
email: 'benjamin@bluemchen.de', email: 'benjamin@bluemchen.de',
creation: [800, 600, 400], creation: [800, 600, 400],
showDetails: false, showDetails: false,
emailChecked: true,
}, },
] ]
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('has only one item itemsList', () => { it('has one item in left table', () => {
expect(wrapper.vm.itemsList).toEqual([ expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
{ })
userId: 1,
firstName: 'Bibi', it('has one item in right table', () => {
lastName: 'Bloxberg', expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
email: 'bibi@bloxberg.de', })
creation: [200, 400, 600],
showDetails: false, it('has the stored user in second row', () => {
}, expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
]) 'benjamin@bluemchen.de',
)
}) })
}) })
@ -265,17 +241,38 @@ describe('Creation', () => {
jest.clearAllMocks() jest.clearAllMocks()
}) })
it('calls API when criteria changes', async () => { describe('search criteria', () => {
await wrapper.setData({ criteria: 'XX' }) beforeEach(async () => {
expect(apolloQueryMock).toBeCalledWith( await wrapper.setData({ criteria: 'XX' })
expect.objectContaining({ })
variables: {
searchText: 'XX', it('calls API when criteria changes', async () => {
currentPage: 1, expect(apolloQueryMock).toBeCalledWith(
pageSize: 25, expect.objectContaining({
}, variables: {
}), searchText: 'XX',
) currentPage: 1,
pageSize: 25,
},
}),
)
})
describe('reset search criteria', () => {
it('calls the API', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
},
}),
)
})
})
}) })
it('calls API when currentPage changes', async () => { it('calls API when currentPage changes', async () => {

View File

@ -3,19 +3,25 @@
<b-row> <b-row>
<b-col cols="12" lg="6"> <b-col cols="12" lg="6">
<label>Usersuche</label> <label>Usersuche</label>
<b-input <b-input-group>
type="text" <b-form-input
v-model="criteria" type="text"
class="shadow p-3 mb-5 bg-white rounded" class="test-input-criteria"
placeholder="User suche" v-model="criteria"
></b-input> :placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
<user-table <user-table
v-if="itemsList.length > 0" v-if="itemsList.length > 0"
type="UserListSearch" type="UserListSearch"
:itemsUser="itemsList" :itemsUser="itemsList"
:fieldsTable="Searchfields" :fieldsTable="Searchfields"
:criteria="criteria"
:creation="creation"
@push-item="pushItem" @push-item="pushItem"
/> />
<b-pagination <b-pagination
@ -27,16 +33,22 @@
></b-pagination> ></b-pagination>
</b-col> </b-col>
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info"> <b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
<user-table <div v-show="itemsMassCreation.length > 0">
v-show="itemsMassCreation.length > 0" <div class="text-right pr-4 mb-1">
class="shadow p-3 mb-5 bg-white rounded" <b-button @click="removeAllBookmarks()" variant="light">
type="UserListMassCreation" <b-icon icon="x" scale="2" variant="danger"></b-icon>
:itemsUser="itemsMassCreation"
:fieldsTable="fields" {{ $t('remove_all') }}
:criteria="null" </b-button>
:creation="creation" </div>
@remove-item="removeItem" <user-table
/> class="shadow p-3 mb-5 bg-white rounded"
type="UserListMassCreation"
:itemsUser="itemsMassCreation"
:fieldsTable="fields"
@remove-item="removeItem"
/>
</div>
<div v-if="itemsMassCreation.length === 0"> <div v-if="itemsMassCreation.length === 0">
{{ $t('multiple_creation_text') }} {{ $t('multiple_creation_text') }}
</div> </div>
@ -45,7 +57,7 @@
type="massCreation" type="massCreation"
:creation="creation" :creation="creation"
:items="itemsMassCreation" :items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmark" @remove-all-bookmark="removeAllBookmarks"
/> />
</b-col> </b-col>
</b-row> </b-row>
@ -55,9 +67,11 @@
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' import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default { export default {
name: 'Creation', name: 'Creation',
mixins: [creationMonths],
components: { components: {
CreationFormular, CreationFormular,
UserTable, UserTable,
@ -69,7 +83,6 @@ export default {
itemsMassCreation: this.$store.state.userSelectedInMassCreation, itemsMassCreation: this.$store.state.userSelectedInMassCreation,
radioSelectedMass: '', radioSelectedMass: '',
criteria: '', criteria: '',
creation: [null, null, null],
rows: 0, rows: 0,
currentPage: 1, currentPage: 1,
perPage: 25, perPage: 25,
@ -126,7 +139,7 @@ export default {
) )
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation) this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
}, },
removeAllBookmark() { removeAllBookmarks() {
this.itemsMassCreation = [] this.itemsMassCreation = []
this.$store.commit('setUserSelectedInMassCreation', []) this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers() this.getUsers()
@ -163,16 +176,6 @@ export default {
{ key: 'bookmark', label: this.$t('remove') }, { key: 'bookmark', label: this.$t('remove') },
] ]
}, },
creationLabel() {
const now = new Date(this.now)
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1)
const beforeLastMonth = new Date(now.getFullYear(), now.getMonth() - 2, 1)
return [
this.$d(beforeLastMonth, 'monthShort'),
this.$d(lastMonth, 'monthShort'),
this.$d(now, 'monthShort'),
].join(' | ')
},
}, },
watch: { watch: {
currentPage() { currentPage() {

View File

@ -78,6 +78,7 @@ describe('CreationConfirm', () => {
it('commits resetOpenCreations to store', () => { it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations') expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
}) })
it('commits setOpenCreations to store', () => { it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2) expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
}) })
@ -85,7 +86,7 @@ describe('CreationConfirm', () => {
describe('remove creation with success', () => { describe('remove creation with success', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit('remove-creation', { id: 1 }) await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
}) })
it('calls the deletePendingCreation mutation', () => { it('calls the deletePendingCreation mutation', () => {
@ -107,7 +108,7 @@ describe('CreationConfirm', () => {
describe('remove creation with error', () => { describe('remove creation with error', () => {
beforeEach(async () => { beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' }) apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit('remove-creation', { id: 1 }) await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
}) })
it('toasts an error message', () => { it('toasts an error message', () => {
@ -118,22 +119,52 @@ describe('CreationConfirm', () => {
describe('confirm creation with success', () => { describe('confirm creation with success', () => {
beforeEach(async () => { beforeEach(async () => {
apolloMutateMock.mockResolvedValue({}) apolloMutateMock.mockResolvedValue({})
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit('confirm-creation', { id: 2 }) await wrapper.findAll('tr').at(2).findAll('button').at(2).trigger('click')
}) })
it('calls the confirmPendingCreation mutation', () => { describe('overlay', () => {
expect(apolloMutateMock).toBeCalledWith({ it('opens the overlay', () => {
mutation: confirmPendingCreation, expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
variables: { id: 2 },
}) })
})
it('commits openCreationsMinus to store', () => { describe('cancel confirmation', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1) beforeEach(async () => {
}) await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
})
it('toasts a success message', () => { it('closes the overlay', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created') expect(wrapper.find('#overlay').isVisible()).toBeFalsy()
})
it('still has 2 items in the table', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
})
})
describe('confirm creation', () => {
beforeEach(async () => {
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
})
it('calls the confirmPendingCreation mutation', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: confirmPendingCreation,
variables: { id: 2 },
})
})
it('commits openCreationsMinus to store', () => {
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
})
it('toasts a success message', () => {
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created')
})
it('has 1 item left in the table', () => {
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
})
})
}) })
}) })

View File

@ -9,10 +9,35 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
userCount: 1, userCount: 1,
userList: [ userList: [
{ {
userId: 1,
firstName: 'Bibi', firstName: 'Bibi',
lastName: 'Bloxberg', lastName: 'Bloxberg',
email: 'bibi@bloxberg.de', email: 'bibi@bloxberg.de',
creation: [200, 400, 600], creation: [200, 400, 600],
emailChecked: true,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
},
{
userId: 3,
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
},
{
userId: 4,
firstName: 'New',
lastName: 'User',
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false, emailChecked: false,
}, },
], ],
@ -24,7 +49,7 @@ const toastErrorMock = jest.fn()
const mocks = { const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$d: jest.fn((d) => d), $d: jest.fn((d) => String(d)),
$apollo: { $apollo: {
query: apolloQueryMock, query: apolloQueryMock,
}, },
@ -42,6 +67,7 @@ describe('UserSearch', () => {
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper() wrapper = Wrapper()
}) })
@ -49,13 +75,90 @@ describe('UserSearch', () => {
expect(wrapper.find('div.user-search').exists()).toBeTruthy() expect(wrapper.find('div.user-search').exists()).toBeTruthy()
}) })
it('calls the API', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: false,
},
}),
)
})
describe('unconfirmed emails', () => { describe('unconfirmed emails', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.find('button.btn-block').trigger('click') await wrapper.find('button.btn-block').trigger('click')
}) })
it('filters the users by unconfirmed emails', () => { it('calls API with filter', () => {
expect(wrapper.vm.searchResult).toHaveLength(1) expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: true,
},
}),
)
})
})
describe('pagination', () => {
beforeEach(async () => {
wrapper.setData({ currentPage: 2 })
})
it('calls the API with new page', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 2,
pageSize: 25,
notActivated: false,
},
}),
)
})
})
describe('user search', () => {
beforeEach(async () => {
wrapper.setData({ criteria: 'search string' })
})
it('calls the API with search string', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'search string',
currentPage: 1,
pageSize: 25,
notActivated: false,
},
}),
)
})
describe('reset the search field', () => {
it('calls the API with empty criteria', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
notActivated: false,
},
}),
)
})
}) })
}) })

View File

@ -7,20 +7,22 @@
</b-button> </b-button>
</div> </div>
<label>{{ $t('user_search') }}</label> <label>{{ $t('user_search') }}</label>
<b-input <div>
type="text" <b-input-group>
v-model="criteria" <b-form-input
class="shadow p-3 mb-3 bg-white rounded" type="text"
:placeholder="$t('user_search')" class="test-input-criteria"
@input="getUsers" v-model="criteria"
></b-input> :placeholder="$t('user_search')"
></b-form-input>
<user-table <b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
type="PageUserSearch" <b-input-group-text class="pointer">
:itemsUser="searchResult" <b-icon icon="x" />
:fieldsTable="fields" </b-input-group-text>
:criteria="criteria" </b-input-group-append>
/> </b-input-group>
</div>
<user-table type="PageUserSearch" :itemsUser="searchResult" :fieldsTable="fields" />
<b-pagination <b-pagination
pills pills
size="lg" size="lg"
@ -35,9 +37,11 @@
<script> <script>
import UserTable from '../components/UserTable.vue' import UserTable from '../components/UserTable.vue'
import { searchUsers } from '../graphql/searchUsers' import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default { export default {
name: 'UserSearch', name: 'UserSearch',
mixins: [creationMonths],
components: { components: {
UserTable, UserTable,
}, },
@ -83,16 +87,11 @@ export default {
currentPage() { currentPage() {
this.getUsers() this.getUsers()
}, },
criteria() {
this.getUsers()
},
}, },
computed: { computed: {
lastMonthDate() {
const now = new Date(this.now)
return new Date(now.getFullYear(), now.getMonth() - 1, 1)
},
beforeLastMonthDate() {
const now = new Date(this.now)
return new Date(now.getFullYear(), now.getMonth() - 2, 1)
},
fields() { fields() {
return [ return [
{ key: 'email', label: this.$t('e_mail') }, { key: 'email', label: this.$t('e_mail') },
@ -100,11 +99,7 @@ export default {
{ key: 'lastName', label: this.$t('lastname') }, { key: 'lastName', label: this.$t('lastname') },
{ {
key: 'creation', key: 'creation',
label: [ label: this.creationLabel,
this.$d(this.beforeLastMonthDate, 'monthShort'),
this.$d(this.lastMonthDate, 'monthShort'),
this.$d(this.now, 'monthShort'),
].join(' | '),
formatter: (value, key, item) => { formatter: (value, key, item) => {
return value.join(' | ') return value.join(' | ')
}, },

View File

@ -4,7 +4,7 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0016-transaction_signatures', DB_VERSION: '0019-replace_login_user_id_with_state_user_id',
} }
const server = { const server = {

View File

@ -21,8 +21,8 @@ import { UserTransaction } from '@entity/UserTransaction'
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction' import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
import { BalanceRepository } from '../../typeorm/repository/Balance' import { BalanceRepository } from '../../typeorm/repository/Balance'
import { calculateDecay } from '../../util/decay' import { calculateDecay } from '../../util/decay'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { AdminPendingCreation } from '@entity/AdminPendingCreation' import { AdminPendingCreation } from '@entity/AdminPendingCreation'
import { User as dbUser } from '@entity/User'
@Resolver() @Resolver()
export class AdminResolver { export class AdminResolver {
@ -378,7 +378,6 @@ function isCreationValid(creations: number[], amount: number, creationDate: Date
} }
async function hasActivatedEmail(email: string): Promise<boolean> { async function hasActivatedEmail(email: string): Promise<boolean> {
const repository = getCustomRepository(LoginUserRepository) const user = await dbUser.findOne({ email })
const user = await repository.findByEmail(email)
return user ? user.emailChecked : false return user ? user.emailChecked : false
} }

View File

@ -33,7 +33,6 @@ import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
import { TransactionTypeId } from '../enum/TransactionTypeId' import { TransactionTypeId } from '../enum/TransactionTypeId'
import { TransactionType } from '../enum/TransactionType' import { TransactionType } from '../enum/TransactionType'
import { hasUserAmount, isHexPublicKey } from '../../util/validate' import { hasUserAmount, isHexPublicKey } from '../../util/validate'
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { RIGHTS } from '../../auth/RIGHTS' import { RIGHTS } from '../../auth/RIGHTS'
// Helper function // Helper function
@ -290,14 +289,13 @@ async function addUserTransaction(
} }
async function getPublicKey(email: string): Promise<string | null> { async function getPublicKey(email: string): Promise<string | null> {
const loginUserRepository = getCustomRepository(LoginUserRepository) const user = await dbUser.findOne({ email: email })
const loginUser = await loginUserRepository.findOne({ email: email })
// User not found // User not found
if (!loginUser) { if (!user) {
return null return null
} }
return loginUser.pubKey.toString('hex') return user.pubKey.toString('hex')
} }
@Resolver() @Resolver()
@ -364,7 +362,7 @@ export class TransactionResolver {
// validate sender user (logged in) // validate sender user (logged in)
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const senderUser = await userRepository.findByPubkeyHex(context.pubKey) const senderUser = await userRepository.findByPubkeyHex(context.pubKey)
if (senderUser.pubkey.length !== 32) { if (senderUser.pubKey.length !== 32) {
throw new Error('invalid sender public key') throw new Error('invalid sender public key')
} }
if (!hasUserAmount(senderUser, amount)) { if (!hasUserAmount(senderUser, amount)) {
@ -454,7 +452,7 @@ export class TransactionResolver {
const transactionSendCoin = new dbTransactionSendCoin() const transactionSendCoin = new dbTransactionSendCoin()
transactionSendCoin.transactionId = transaction.id transactionSendCoin.transactionId = transaction.id
transactionSendCoin.userId = senderUser.id transactionSendCoin.userId = senderUser.id
transactionSendCoin.senderPublic = senderUser.pubkey transactionSendCoin.senderPublic = senderUser.pubKey
transactionSendCoin.recipiantUserId = recipiantUser.id transactionSendCoin.recipiantUserId = recipiantUser.id
transactionSendCoin.recipiantPublic = Buffer.from(recipiantPublicKey, 'hex') transactionSendCoin.recipiantPublic = Buffer.from(recipiantPublicKey, 'hex')
transactionSendCoin.amount = centAmount transactionSendCoin.amount = centAmount

View File

@ -14,11 +14,8 @@ import UnsecureLoginArgs from '../arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs' import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs'
import { klicktippNewsletterStateMiddleware } from '../../middleware/klicktippMiddleware' import { klicktippNewsletterStateMiddleware } from '../../middleware/klicktippMiddleware'
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 { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail' import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail' import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
@ -27,7 +24,6 @@ import { klicktippSignIn } from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS' import { RIGHTS } from '../../auth/RIGHTS'
import { ServerUserRepository } from '../../typeorm/repository/ServerUser' import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
import { ROLE_ADMIN } from '../../auth/ROLES' import { ROLE_ADMIN } from '../../auth/ROLES'
import { randomBytes } from 'crypto'
const EMAIL_OPT_IN_RESET_PASSWORD = 2 const EMAIL_OPT_IN_RESET_PASSWORD = 2
const EMAIL_OPT_IN_REGISTER = 1 const EMAIL_OPT_IN_REGISTER = 1
@ -186,10 +182,10 @@ const createEmailOptIn = async (
return emailOptIn return emailOptIn
} }
const getOptInCode = async (loginUser: LoginUser): Promise<LoginEmailOptIn> => { const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn) const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let optInCode = await loginEmailOptInRepository.findOne({ let optInCode = await loginEmailOptInRepository.findOne({
userId: loginUser.id, userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD, emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
}) })
@ -207,7 +203,7 @@ const getOptInCode = async (loginUser: LoginUser): Promise<LoginEmailOptIn> => {
} else { } else {
optInCode = new LoginEmailOptIn() optInCode = new LoginEmailOptIn()
optInCode.verificationCode = random(64) optInCode.verificationCode = random(64)
optInCode.userId = loginUser.id optInCode.userId = loginUserId
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
} }
await loginEmailOptInRepository.save(optInCode) await loginEmailOptInRepository.save(optInCode)
@ -223,17 +219,15 @@ export class UserResolver {
// TODO refactor and do not have duplicate code with login(see below) // TODO refactor and do not have duplicate code with login(see below)
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey) const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findByEmail(userEntity.email)
const user = new User() const user = new User()
user.id = userEntity.id user.id = userEntity.id
user.email = userEntity.email user.email = userEntity.email
user.firstName = userEntity.firstName user.firstName = userEntity.firstName
user.lastName = userEntity.lastName user.lastName = userEntity.lastName
user.username = userEntity.username user.username = userEntity.username
user.description = loginUser.description user.description = userEntity.description
user.pubkey = userEntity.pubkey.toString('hex') user.pubkey = userEntity.pubKey.toString('hex')
user.language = loginUser.language user.language = userEntity.language
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context) user.hasElopage = await this.hasElopage(context)
@ -259,76 +253,50 @@ export class UserResolver {
@Ctx() context: any, @Ctx() context: any,
): Promise<User> { ): Promise<User> {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
const loginUserRepository = getCustomRepository(LoginUserRepository) const dbUser = await DbUser.findOneOrFail({ email }).catch(() => {
const loginUser = await loginUserRepository.findByEmail(email).catch(() => {
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
}) })
if (!loginUser.emailChecked) { if (!dbUser.emailChecked) {
throw new Error('User email not validated') throw new Error('User email not validated')
} }
if (loginUser.password === BigInt(0)) { if (dbUser.password === BigInt(0)) {
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no password set yet') throw new Error('User has no password set yet')
} }
if (!loginUser.pubKey || !loginUser.privKey) { if (!dbUser.pubKey || !dbUser.privKey) {
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code // TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no private or publicKey') throw new Error('User has no private or publicKey')
} }
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
const loginUserPassword = BigInt(loginUser.password.toString()) const loginUserPassword = BigInt(dbUser.password.toString())
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
throw new Error('No user with this credentials') throw new Error('No user with this credentials')
} }
// TODO: If user has no pubKey Create it again and update user.
const userRepository = getCustomRepository(UserRepository)
let userEntity: void | DbUser
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 = 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')
})
})
if (!userEntity) {
throw new Error('error with cannot happen')
}
const user = new User() const user = new User()
user.id = userEntity.id user.id = dbUser.id
user.email = email user.email = email
user.firstName = loginUser.firstName user.firstName = dbUser.firstName
user.lastName = loginUser.lastName user.lastName = dbUser.lastName
user.username = loginUser.username user.username = dbUser.username
user.description = loginUser.description user.description = dbUser.description
user.pubkey = loginUserPubKeyString user.pubkey = dbUser.pubKey.toString('hex')
user.language = loginUser.language user.language = dbUser.language
// Elopage Status & Stored PublisherId // Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ pubKey: loginUserPubKeyString }) user.hasElopage = await this.hasElopage({ pubKey: dbUser.pubKey.toString('hex') })
if (!user.hasElopage && publisherId) { if (!user.hasElopage && publisherId) {
user.publisherId = publisherId user.publisherId = publisherId
// TODO: Check if we can use updateUserInfos // TODO: Check if we can use updateUserInfos
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey }) // await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
const loginUserRepository = getCustomRepository(LoginUserRepository) dbUser.publisherId = publisherId
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email }) DbUser.save(dbUser)
loginUser.publisherId = publisherId
loginUserRepository.save(loginUser)
} }
// coinAnimation // coinAnimation
const userSettingRepository = getCustomRepository(UserSettingRepository) const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION) .readBoolean(dbUser.id, Setting.COIN_ANIMATION)
.catch((error) => { .catch((error) => {
throw new Error(error) throw new Error(error)
}) })
@ -341,7 +309,7 @@ export class UserResolver {
context.setHeaders.push({ context.setHeaders.push({
key: 'token', key: 'token',
value: encode(loginUser.pubKey), value: encode(dbUser.pubKey),
}) })
return user return user
@ -393,18 +361,21 @@ export class UserResolver {
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) // const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
const emailHash = getEmailHash(email) const emailHash = getEmailHash(email)
// Table: login_users // Table: state_users
const loginUser = new LoginUser() const dbUser = new DbUser()
loginUser.email = email dbUser.email = email
loginUser.firstName = firstName dbUser.firstName = firstName
loginUser.lastName = lastName dbUser.lastName = lastName
loginUser.username = username dbUser.username = username
loginUser.description = '' dbUser.description = ''
dbUser.emailHash = emailHash
dbUser.language = language
dbUser.publisherId = publisherId
dbUser.passphrase = passphrase.join(' ')
// TODO this field has no null allowed unlike the loginServer table
// dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
// dbUser.pubkey = keyPair[0]
// loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash // 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.pubKey = keyPair[0]
// loginUser.privKey = encryptedPrivkey // loginUser.privKey = encryptedPrivkey
@ -412,43 +383,15 @@ export class UserResolver {
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
const { id: loginUserId } = await queryRunner.manager.save(loginUser).catch((error) => { await queryRunner.manager.save(dbUser).catch((error) => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('insert LoginUser failed', error) console.log('Error while saving dbUser', error)
throw new Error('insert user failed')
})
// 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;
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.email = email
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.username = username
// TODO this field has no null allowed unlike the loginServer table
dbUser.pubkey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
// dbUser.pubkey = keyPair[0]
await queryRunner.manager.save(dbUser).catch((er) => {
// eslint-disable-next-line no-console
console.log('Error while saving dbUser', er)
throw new Error('error saving user') throw new Error('error saving user')
}) })
// Store EmailOptIn in DB // Store EmailOptIn in DB
// TODO: this has duplicate code with sendResetPasswordEmail // TODO: this has duplicate code with sendResetPasswordEmail
const emailOptIn = await createEmailOptIn(loginUserId, queryRunner) const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{code}/g, /{code}/g,
@ -480,15 +423,14 @@ export class UserResolver {
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> { async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
const loginUserRepository = getCustomRepository(LoginUserRepository) const user = await DbUser.findOneOrFail({ email: email })
const loginUser = await loginUserRepository.findOneOrFail({ email: email })
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
const emailOptIn = await createEmailOptIn(loginUser.id, queryRunner) const emailOptIn = await createEmailOptIn(user.id, queryRunner)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{code}/g, /{code}/g,
@ -497,8 +439,8 @@ export class UserResolver {
const emailSent = await sendAccountActivationEmail({ const emailSent = await sendAccountActivationEmail({
link: activationLink, link: activationLink,
firstName: loginUser.firstName, firstName: user.firstName,
lastName: loginUser.lastName, lastName: user.lastName,
email, email,
}) })
@ -522,10 +464,9 @@ export class UserResolver {
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> { async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
// TODO: this has duplicate code with createUser // TODO: this has duplicate code with createUser
const loginUserRepository = await getCustomRepository(LoginUserRepository) const user = await DbUser.findOneOrFail({ email })
const loginUser = await loginUserRepository.findOneOrFail({ email })
const optInCode = await getOptInCode(loginUser) const optInCode = await getOptInCode(user.id)
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
/{code}/g, /{code}/g,
@ -534,8 +475,8 @@ export class UserResolver {
const emailSent = await sendResetPasswordEmail({ const emailSent = await sendResetPasswordEmail({
link, link,
firstName: loginUser.firstName, firstName: user.firstName,
lastName: loginUser.lastName, lastName: user.lastName,
email, email,
}) })
@ -575,34 +516,18 @@ export class UserResolver {
throw new Error('Code is older than 10 minutes') throw new Error('Code is older than 10 minutes')
} }
// load loginUser
const loginUserRepository = await getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository
.findOneOrFail({ id: optInCode.userId })
.catch(() => {
throw new Error('Could not find corresponding Login User')
})
// load user // load user
const dbUserRepository = await getCustomRepository(UserRepository) const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
const dbUser = await dbUserRepository.findOneOrFail({ email: loginUser.email }).catch(() => { throw new Error('Could not find corresponding Login User')
throw new Error('Could not find corresponding User')
}) })
const loginUserBackupRepository = await getRepository(LoginUserBackup)
let loginUserBackup = await loginUserBackupRepository.findOne({ userId: loginUser.id })
// Generate Passphrase if needed // Generate Passphrase if needed
if (!loginUserBackup) { if (!user.passphrase) {
const passphrase = PassphraseGenerate() const passphrase = PassphraseGenerate()
loginUserBackup = new LoginUserBackup() user.passphrase = passphrase.join(' ')
loginUserBackup.userId = loginUser.id
loginUserBackup.passphrase = passphrase.join(' ') // login server saves trailing space
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
loginUserBackupRepository.save(loginUserBackup)
} }
const passphrase = loginUserBackup.passphrase.split(' ') const passphrase = user.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) { if (passphrase.length < PHRASE_WORD_COUNT) {
// TODO if this can happen we cannot recover from that // TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont // this seem to be good on production data, if we dont
@ -611,29 +536,23 @@ export class UserResolver {
} }
// Activate EMail // Activate EMail
loginUser.emailChecked = true user.emailChecked = true
// Update Password // Update Password
const passwordHash = SecretKeyCryptographyCreateKey(loginUser.email, password) // return short and long hash const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
loginUser.pubKey = keyPair[0] user.pubKey = keyPair[0]
loginUser.privKey = encryptedPrivkey user.privKey = encryptedPrivkey
dbUser.pubkey = keyPair[0]
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED') await queryRunner.startTransaction('READ UNCOMMITTED')
try { try {
// Save loginUser
await queryRunner.manager.save(loginUser).catch((error) => {
throw new Error('error saving loginUser: ' + error)
})
// Save user // Save user
await queryRunner.manager.save(dbUser).catch((error) => { await queryRunner.manager.save(user).catch((error) => {
throw new Error('error saving user: ' + error) throw new Error('error saving user: ' + error)
}) })
@ -654,12 +573,7 @@ export class UserResolver {
// TODO do we always signUp the user? How to handle things with old users? // TODO do we always signUp the user? How to handle things with old users?
if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) { if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) {
try { try {
await klicktippSignIn( await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
loginUser.email,
loginUser.language,
loginUser.firstName,
loginUser.lastName,
)
} catch { } catch {
// TODO is this a problem? // TODO is this a problem?
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -689,8 +603,6 @@ export class UserResolver {
): Promise<boolean> { ): Promise<boolean> {
const userRepository = getCustomRepository(UserRepository) const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey) const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
const loginUserRepository = getCustomRepository(LoginUserRepository)
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
if (username) { if (username) {
throw new Error('change username currently not supported!') throw new Error('change username currently not supported!')
@ -704,46 +616,44 @@ export class UserResolver {
} }
if (firstName) { if (firstName) {
loginUser.firstName = firstName
userEntity.firstName = firstName userEntity.firstName = firstName
} }
if (lastName) { if (lastName) {
loginUser.lastName = lastName
userEntity.lastName = lastName userEntity.lastName = lastName
} }
if (description) { if (description) {
loginUser.description = description userEntity.description = description
} }
if (language) { if (language) {
if (!isLanguage(language)) { if (!isLanguage(language)) {
throw new Error(`"${language}" isn't a valid language`) throw new Error(`"${language}" isn't a valid language`)
} }
loginUser.language = language userEntity.language = language
} }
if (password && passwordNew) { if (password && passwordNew) {
// TODO: This had some error cases defined - like missing private key. This is no longer checked. // TODO: This had some error cases defined - like missing private key. This is no longer checked.
const oldPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, password) const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
if (BigInt(loginUser.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
throw new Error(`Old password is invalid`) throw new Error(`Old password is invalid`)
} }
const privKey = SecretKeyCryptographyDecrypt(loginUser.privKey, oldPasswordHash[1]) const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
const newPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, passwordNew) // return short and long hash const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1]) const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
// Save new password hash and newly encrypted private key // Save new password hash and newly encrypted private key
loginUser.password = newPasswordHash[0].readBigUInt64LE() userEntity.password = newPasswordHash[0].readBigUInt64LE()
loginUser.privKey = encryptedPrivkey userEntity.privKey = encryptedPrivkey
} }
// Save publisherId only if Elopage is not yet registered // Save publisherId only if Elopage is not yet registered
if (publisherId && !(await this.hasElopage(context))) { if (publisherId && !(await this.hasElopage(context))) {
loginUser.publisherId = publisherId userEntity.publisherId = publisherId
} }
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
@ -760,10 +670,6 @@ export class UserResolver {
}) })
} }
await queryRunner.manager.save(loginUser).catch((error) => {
throw new Error('error saving loginUser: ' + error)
})
await queryRunner.manager.save(userEntity).catch((error) => { await queryRunner.manager.save(userEntity).catch((error) => {
throw new Error('error saving user: ' + error) throw new Error('error saving user: ' + error)
}) })
@ -793,7 +699,7 @@ export class UserResolver {
throw new Error(`Username must be at minimum ${MIN_CHARACTERS_USERNAME} characters long.`) throw new Error(`Username must be at minimum ${MIN_CHARACTERS_USERNAME} characters long.`)
} }
const usersFound = await LoginUser.count({ username }) const usersFound = await DbUser.count({ username })
// Username already present? // Username already present?
if (usersFound !== 0) { if (usersFound !== 0) {

View File

@ -1,24 +0,0 @@
import { EntityRepository, Repository } from '@dbTools/typeorm'
import { LoginUser } from '@entity/LoginUser'
@EntityRepository(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

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

View File

@ -5,7 +5,7 @@ import { User } from '@entity/User'
export class UserRepository extends Repository<User> { export class UserRepository extends Repository<User> {
async findByPubkeyHex(pubkeyHex: string): Promise<User> { async findByPubkeyHex(pubkeyHex: string): Promise<User> {
return this.createQueryBuilder('user') return this.createQueryBuilder('user')
.where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex }) .where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
.getOneOrFail() .getOneOrFail()
} }

View File

@ -31,7 +31,7 @@ import { LoginElopageBuys } from '@entity/LoginElopageBuys'
import { getCustomRepository } from '@dbTools/typeorm' import { getCustomRepository } from '@dbTools/typeorm'
import { UserResolver } from '../graphql/resolver/UserResolver' import { UserResolver } from '../graphql/resolver/UserResolver'
import { LoginElopageBuysRepository } from '../typeorm/repository/LoginElopageBuys' import { LoginElopageBuysRepository } from '../typeorm/repository/LoginElopageBuys'
import { LoginUserRepository } from '../typeorm/repository/LoginUser' import { User as dbUser } from '@entity/User'
export const elopageWebhook = async (req: any, res: any): Promise<void> => { export const elopageWebhook = async (req: any, res: any): Promise<void> => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -114,8 +114,7 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
} }
// Do we already have such a user? // Do we already have such a user?
const loginUserRepository = await getCustomRepository(LoginUserRepository) if ((await dbUser.count({ email })) !== 0) {
if ((await loginUserRepository.count({ email })) !== 0) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`Did not create User - already exists with email: ${email}`) console.log(`Did not create User - already exists with email: ${email}`)
return return

View File

@ -7,13 +7,13 @@ export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true }) @PrimaryGeneratedColumn('increment', { unsigned: true })
id: number id: number
@Column({ name: 'index_id', default: 0 }) @Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number indexId: number
@Column({ name: 'group_id', default: 0, unsigned: true }) @Column({ name: 'group_id', default: 0, unsigned: true })
groupId: number groupId: number
@Column({ type: 'binary', length: 32, name: 'public_key' }) @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubkey: Buffer pubkey: Buffer
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' }) @Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
@ -40,7 +40,7 @@ export class User extends BaseEntity {
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' }) @Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string username: string
@Column() @Column({ type: 'bool', default: false })
disabled: boolean disabled: boolean
@OneToOne(() => Balance, (balance) => balance.user) @OneToOne(() => Balance, (balance) => balance.user)

View File

@ -7,13 +7,13 @@ export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true }) @PrimaryGeneratedColumn('increment', { unsigned: true })
id: number id: number
@Column({ name: 'index_id', default: 0 }) @Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number indexId: number
@Column({ name: 'group_id', default: 0, unsigned: true }) @Column({ name: 'group_id', default: 0, unsigned: true })
groupId: number groupId: number
@Column({ type: 'binary', length: 32, name: 'public_key' }) @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubkey: Buffer pubkey: Buffer
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' }) @Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
@ -40,7 +40,7 @@ export class User extends BaseEntity {
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' }) @Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string username: string
@Column() @Column({ type: 'bool', default: false })
disabled: boolean disabled: boolean
@OneToMany(() => UserSetting, (userSetting) => userSetting.user) @OneToMany(() => UserSetting, (userSetting) => userSetting.user)

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { LoginUserBackup } from '../LoginUserBackup' import { LoginUserBackup } from './LoginUserBackup'
// Moriz: I do not like the idea of having two user tables // Moriz: I do not like the idea of having two user tables
@Entity('login_users') @Entity('login_users')
@ -19,7 +19,7 @@ export class LoginUser extends BaseEntity {
@Column({ length: 255, default: '' }) @Column({ length: 255, default: '' })
username: string username: string
@Column({ default: '', nullable: true }) @Column({ type: 'mediumtext', default: '', nullable: true })
description: string description: string
@Column({ type: 'bigint', default: 0, unsigned: true }) @Column({ type: 'bigint', default: 0, unsigned: true })
@ -34,19 +34,19 @@ export class LoginUser extends BaseEntity {
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' }) @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date createdAt: Date
@Column({ name: 'email_checked', default: 0 }) @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean emailChecked: boolean
@Column({ name: 'passphrase_shown', default: 0 }) @Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean passphraseShown: boolean
@Column({ length: 4, default: 'de' }) @Column({ length: 4, default: 'de', nullable: false })
language: string language: string
@Column({ default: 0 }) @Column({ type: 'bool', default: false })
disabled: boolean disabled: boolean
@Column({ name: 'group_id', default: 0, unsigned: true }) @Column({ name: 'group_id', default: 0, unsigned: true })

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, JoinColumn, OneToOne } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, JoinColumn, OneToOne } from 'typeorm'
import { LoginUser } from '../LoginUser' import { LoginUser } from './LoginUser'
@Entity('login_user_backups') @Entity('login_user_backups')
export class LoginUserBackup extends BaseEntity { export class LoginUserBackup extends BaseEntity {

View File

@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm' import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm'
import { LoginUserBackup } from '../LoginUserBackup' import { LoginUserBackup } from '../0003-login_server_tables/LoginUserBackup'
// Moriz: I do not like the idea of having two user tables // Moriz: I do not like the idea of having two user tables
@Entity('login_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) @Entity('login_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
@ -19,7 +19,7 @@ export class LoginUser extends BaseEntity {
@Column({ length: 255, default: '', collation: 'utf8mb4_unicode_ci' }) @Column({ length: 255, default: '', collation: 'utf8mb4_unicode_ci' })
username: string username: string
@Column({ default: '', collation: 'utf8mb4_unicode_ci', nullable: true }) @Column({ type: 'mediumtext', default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
description: string description: string
@Column({ type: 'bigint', default: 0, unsigned: true }) @Column({ type: 'bigint', default: 0, unsigned: true })
@ -34,19 +34,19 @@ export class LoginUser extends BaseEntity {
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' }) @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date createdAt: Date
@Column({ name: 'email_checked', default: 0 }) @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean emailChecked: boolean
@Column({ name: 'passphrase_shown', default: 0 }) @Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean passphraseShown: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci' }) @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string language: string
@Column({ default: 0 }) @Column({ type: 'bool', default: false })
disabled: boolean disabled: boolean
@Column({ name: 'group_id', default: 0, unsigned: true }) @Column({ name: 'group_id', default: 0, unsigned: true })

View File

@ -0,0 +1,16 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('login_user_backups')
export class LoginUserBackup extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ type: 'text', name: 'passphrase', nullable: false })
passphrase: string
@Column({ name: 'user_id', nullable: false })
userId: number
@Column({ name: 'mnemonic_type', default: -1 })
mnemonicType: number
}

View File

@ -0,0 +1,74 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'login_user_id', default: null, unsigned: true })
loginUserId: number
@Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string
@Column({ type: 'bool', default: false })
disabled: boolean
@Column({ type: 'mediumtext', default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
settings: UserSetting[]
}

View File

@ -0,0 +1,83 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'login_user_id', default: null, unsigned: true })
loginUserId: number
@Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string
@Column({ type: 'bool', default: false })
disabled: boolean
@Column({ type: 'mediumtext', default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@Column({
type: 'text',
name: 'passphrase',
collation: 'utf8mb4_unicode_ci',
nullable: true,
default: null,
})
passphrase: string
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
settings: UserSetting[]
}

View File

@ -0,0 +1,80 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
import { UserSetting } from '../UserSetting'
@Entity('state_users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubKey: Buffer
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
privKey: Buffer
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
email: string
@Column({
name: 'first_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
firstName: string
@Column({
name: 'last_name',
length: 255,
nullable: true,
default: null,
collation: 'utf8mb4_unicode_ci',
})
lastName: string
@Column({ length: 255, nullable: true, default: null, collation: 'utf8mb4_unicode_ci' })
username: string
@Column({ type: 'bool', default: false })
disabled: boolean
@Column({ type: 'mediumtext', default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
description: string
@Column({ type: 'bigint', default: 0, unsigned: true })
password: BigInt
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
emailHash: Buffer
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
@Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
@Column({ name: 'publisher_id', default: 0 })
publisherId: number
@Column({
type: 'text',
name: 'passphrase',
collation: 'utf8mb4_unicode_ci',
nullable: true,
default: null,
})
passphrase: string
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
settings: UserSetting[]
}

View File

@ -1 +0,0 @@
export { LoginUser } from './0006-login_users_collation/LoginUser'

View File

@ -1 +0,0 @@
export { LoginUserBackup } from './0003-login_server_tables/LoginUserBackup'

View File

@ -1 +1 @@
export { User } from './0002-add_settings/User' export { User } from './0019-replace_login_user_id_with_state_user_id/User'

View File

@ -1,8 +1,6 @@
import { Balance } from './Balance' import { Balance } from './Balance'
import { LoginElopageBuys } from './LoginElopageBuys' import { LoginElopageBuys } from './LoginElopageBuys'
import { LoginEmailOptIn } from './LoginEmailOptIn' import { LoginEmailOptIn } from './LoginEmailOptIn'
import { LoginUser } from './LoginUser'
import { LoginUserBackup } from './LoginUserBackup'
import { Migration } from './Migration' import { Migration } from './Migration'
import { ServerUser } from './ServerUser' import { ServerUser } from './ServerUser'
import { Transaction } from './Transaction' import { Transaction } from './Transaction'
@ -18,8 +16,6 @@ export const entities = [
Balance, Balance,
LoginElopageBuys, LoginElopageBuys,
LoginEmailOptIn, LoginEmailOptIn,
LoginUser,
LoginUserBackup,
Migration, Migration,
ServerUser, ServerUser,
Transaction, Transaction,

View File

@ -0,0 +1,150 @@
/* MIGRATION TO COMBINE LOGIN_USERS WITH STATE_USERS TABLE
*
* This migration combines the table `login_users` with
* the `state_users` table, where the later is the target.
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Drop column `group_id` since it contains uniform data which is not the same as the uniform data
// on login_users. Since we do not need this data anyway, we sjust throw it away.
await queryFn('ALTER TABLE `state_users` DROP COLUMN `group_id`;')
// Remove the unique constraint from the pubkey
await queryFn('ALTER TABLE `state_users` DROP INDEX `public_key`;')
// Allow NULL on the `state_users` pubkey like it is allowed on `login_users`
await queryFn('ALTER TABLE `state_users` MODIFY COLUMN `public_key` binary(32) DEFAULT NULL;')
// instead use a unique constraint for the email like on `login_users`
// therefore do not allow null on `email` anymore
await queryFn(
'ALTER TABLE `state_users` MODIFY COLUMN `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL;',
)
await queryFn('ALTER TABLE `state_users` ADD CONSTRAINT `email` UNIQUE KEY (`email`);')
// Create `login_user_id` column - to store the login_users.id field to not break references.
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `login_user_id` int(10) unsigned DEFAULT NULL AFTER `id`;',
)
// Create missing data columns for the data stored in `login_users`
await queryFn(
"ALTER TABLE `state_users` ADD COLUMN `description` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT '' AFTER `disabled`;",
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `password` bigint(20) unsigned DEFAULT 0 AFTER `description`;',
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `privkey` binary(80) DEFAULT NULL AFTER `public_key`;',
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `email_hash` binary(32) DEFAULT NULL AFTER `password`;',
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `created` datetime NOT NULL DEFAULT current_timestamp() AFTER `email_hash`;',
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `email_checked` tinyint(4) NOT NULL DEFAULT 0 AFTER `created`;',
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `passphrase_shown` tinyint(4) NOT NULL DEFAULT 0 AFTER `email_checked`;',
)
await queryFn(
"ALTER TABLE `state_users` ADD COLUMN `language` varchar(4) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'de' AFTER `passphrase_shown`;",
)
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `publisher_id` int(11) DEFAULT 0 AFTER `language`;',
)
// Move data from `login_users` to the newly modified `state_users` table.
// The following rules for overwriting data applies:
// email is the matching criteria
// public_key is overwritten by `login_users`.`pubkey` (we have validated the passphrases here) (2 keys differ)
// first_name is more accurate on `state_users` and stays unchanged (1 users with different first_* & last_name)
// last_name is more accurate on `state_users` and stays unchanged (1 users with different first_* & last_name)
// username does not contain any relevant data, either NULL or '' and therefore we do not change anything here
// disabled does not differ, we can omit it
await queryFn(`
UPDATE state_users
LEFT JOIN login_users ON state_users.email = login_users.email
SET state_users.public_key = login_users.pubkey,
state_users.login_user_id = login_users.id,
state_users.description = login_users.description,
state_users.password = login_users.password,
state_users.privkey = login_users.privkey,
state_users.email_hash = login_users.email_hash,
state_users.created = login_users.created,
state_users.email_checked = login_users.email_checked,
state_users.passphrase_shown = login_users.passphrase_shown,
state_users.language = login_users.language,
state_users.publisher_id = login_users.publisher_id
;
`)
// Drop `login_users` table
await queryFn('DROP TABLE `login_users`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE \`login_users\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`email\` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL,
\`first_name\` varchar(150) COLLATE utf8mb4_unicode_ci NOT NULL,
\`last_name\` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '',
\`username\` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '',
\`description\` mediumtext COLLATE utf8mb4_unicode_ci DEFAULT '',
\`password\` bigint(20) unsigned DEFAULT 0,
\`pubkey\` binary(32) DEFAULT NULL,
\`privkey\` binary(80) DEFAULT NULL,
\`email_hash\` binary(32) DEFAULT NULL,
\`created\` datetime NOT NULL DEFAULT current_timestamp(),
\`email_checked\` tinyint(4) NOT NULL DEFAULT 0,
\`passphrase_shown\` tinyint(4) NOT NULL DEFAULT 0,
\`language\` varchar(4) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'de',
\`disabled\` tinyint(4) DEFAULT 0,
\`group_id\` int(10) unsigned DEFAULT 0,
\`publisher_id\` int(11) DEFAULT 0,
PRIMARY KEY (\`id\`),
UNIQUE KEY \`email\` (\`email\`)
) ENGINE=InnoDB AUTO_INCREMENT=2363 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`)
await queryFn(`
INSERT INTO login_users
( id, email, first_name, last_name, username,
description, password, pubkey, privkey, email_hash,
created, email_checked, passphrase_shown, language,
disabled, group_id, publisher_id )
( SELECT login_user_id AS id, email, first_name,
last_name, username, description, password,
public_key AS pubkey, privkey, email_hash,
created, email_checked, passphrase_shown,
language, disabled, '1' AS group_id,
publisher_id
FROM state_users )
;
`)
await queryFn('ALTER TABLE `state_users` DROP COLUMN `publisher_id`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `language`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `passphrase_shown`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `email_checked`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `created`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `email_hash`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `privkey`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `password`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `description`;')
await queryFn('ALTER TABLE `state_users` DROP COLUMN `login_user_id`;')
await queryFn('ALTER TABLE `state_users` DROP INDEX `email`;')
await queryFn(
'ALTER TABLE `state_users` MODIFY COLUMN `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL;',
)
// Note: if the public_key is NULL, we need to set a random key in order to meet the constraint
await queryFn(
'UPDATE `state_users` SET public_key = UNHEX(SHA1(RAND())) WHERE public_key IS NULL;',
)
await queryFn('ALTER TABLE `state_users` MODIFY COLUMN `public_key` binary(32) NOT NULL;')
await queryFn('ALTER TABLE `state_users` ADD CONSTRAINT `public_key` UNIQUE KEY (`public_key`);')
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `group_id` int(10) unsigned NOT NULL DEFAULT 0 AFTER index_id;',
)
}

View File

@ -0,0 +1,48 @@
/* MIGRATION TO COMBINE LOGIN_BACKUP_USERS TABLE WITH STATE_USERS
*
* This migration combines the table `login_user_backups` into
* the `state_users` table, where the later is the target.
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// We only keep the passphrase, the mnemonic type is a constant,
// since every passphrase was converted to mnemonic type 2
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `passphrase` text DEFAULT NULL AFTER `publisher_id`;',
)
// Move data from `login_user_backups` to the newly modified `state_users` table.
await queryFn(`
UPDATE state_users
LEFT JOIN login_user_backups ON state_users.login_user_id = login_user_backups.user_id
SET state_users.passphrase = login_user_backups.passphrase
WHERE login_user_backups.passphrase IS NOT NULL
;
`)
// Drop `login_user_backups` table
await queryFn('DROP TABLE `login_user_backups`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`
CREATE TABLE \`login_user_backups\` (
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
\`user_id\` int(11) NOT NULL,
\`passphrase\` text NOT NULL,
\`mnemonic_type\` int(11) DEFAULT -1,
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB AUTO_INCREMENT=1862 DEFAULT CHARSET=utf8mb4;
`)
await queryFn(`
INSERT INTO login_user_backups
( user_id, passphrase, mnemonic_type )
( SELECT login_user_id AS user_id,
passphrase,
'2' as mnemonic_type
FROM state_users
WHERE passphrase IS NOT NULL )
;
`)
await queryFn('ALTER TABLE `state_users` DROP COLUMN `passphrase`;')
}

View File

@ -0,0 +1,57 @@
/* MIGRATION TO REPLACE LOGIN_USER_ID WITH STATE_USER_ID
*
* This migration replaces the `login_user_id with` the
* `state_user.id` and removes corresponding columns.
* The table affected is `login_email_opt_in`
*/
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// Delete email opt in codes which can not be linked to an user
await queryFn(`
DELETE FROM \`login_email_opt_in\`
WHERE user_id NOT IN
( SELECT login_user_id FROM state_users )
`)
// Replace user_id in `login_email_opt_in`
await queryFn(`
UPDATE login_email_opt_in
LEFT JOIN state_users ON state_users.login_user_id = login_email_opt_in.user_id
SET login_email_opt_in.user_id = state_users.id;
`)
// Remove the column `login_user_id` from `state_users`
await queryFn('ALTER TABLE `state_users` DROP COLUMN `login_user_id`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `state_users` ADD COLUMN `login_user_id` int(10) unsigned DEFAULT NULL AFTER `id`;',
)
// Instead of generating new `login_user_id`'s we just use the id of state user.
// This way we do not need to alter the `user_id`'s of `login_email_opt_in` table
// at all when migrating down.
// This is possible since there are no old `login_user.id` referenced anymore and
// we can freely choose them
await queryFn('UPDATE `state_users` SET login_user_id = id')
// Insert back broken data, since we generate new `user_id`'s the old data might be now
// linked to existing accounts. To prevent that all invalid `user_id`'s are now negative.
// This renders them invalid while still keeping the original value
await queryFn(`
INSERT INTO login_email_opt_in
(id, user_id, verification_code, email_opt_in_type_id, created, resend_count, updated)
VALUES
('38','-41','7544440030630126261','0','2019-11-09 13:58:21','0','2020-07-17 13:58:29'),
('1262','-1185','2702555860489093775','3','2020-10-17 00:57:29','0','2020-10-17 00:57:29'),
('1431','-1319','9846213635571107141','3','2020-12-29 00:07:32','0','2020-12-29 00:07:32'),
('1548','-1185','1009203004512986277','1','2021-01-26 01:07:29','0','2021-01-26 01:07:29'),
('1549','-1185','2144334450300724903','1','2021-01-26 01:07:32','0','2021-01-26 01:07:32'),
('1683','-1525','14803676216828342915','3','2021-03-10 08:39:39','0','2021-03-10 08:39:39'),
('1899','-1663','16616172057370363741','3','2021-04-12 14:49:18','0','2021-04-12 14:49:18'),
('2168','-1865','13129474130315401087','3','2021-07-08 11:58:54','0','2021-07-08 11:58:54'),
('2274','-1935','5775135935896874129','3','2021-08-24 11:40:04','0','2021-08-24 11:40:04'),
('2318','-1967','5713731625139303791','3','2021-09-06 21:38:30','0','2021-09-06 21:38:30'),
('2762','-2263','6997866521554931275','1','2021-12-25 11:44:30','0','2021-12-25 11:44:30');
`)
}

View File

@ -1,18 +0,0 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { LoginUserBackup } from '../../entity/LoginUserBackup'
import { LoginUserBackupContext } from '../interface/UserContext'
define(LoginUserBackup, (faker: typeof Faker, context?: LoginUserBackupContext) => {
if (!context || !context.userId) {
throw new Error('LoginUserBackup: No userId present!')
}
const userBackup = new LoginUserBackup()
// TODO: Get the real passphrase
userBackup.passphrase = context.passphrase ? context.passphrase : faker.random.words(24)
userBackup.mnemonicType = context.mnemonicType ? context.mnemonicType : 2
userBackup.userId = context.userId
return userBackup
})

View File

@ -1,30 +0,0 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { LoginUser } from '../../entity/LoginUser'
import { randomBytes } from 'crypto'
import { LoginUserContext } from '../interface/UserContext'
define(LoginUser, (faker: typeof Faker, context?: LoginUserContext) => {
if (!context) context = {}
const user = new LoginUser()
user.email = context.email ? context.email : faker.internet.email()
user.firstName = context.firstName ? context.firstName : faker.name.firstName()
user.lastName = context.lastName ? context.lastName : faker.name.lastName()
user.username = context.username ? context.username : faker.internet.userName()
user.description = context.description ? context.description : faker.random.words(4)
// TODO Create real password and keys/hash
user.password = context.password ? context.password : BigInt(0)
user.pubKey = context.pubKey ? context.pubKey : randomBytes(32)
user.privKey = context.privKey ? context.privKey : randomBytes(80)
user.emailHash = context.emailHash ? context.emailHash : randomBytes(32)
user.createdAt = context.createdAt ? context.createdAt : faker.date.recent()
user.emailChecked = context.emailChecked === undefined ? false : context.emailChecked
user.passphraseShown = context.passphraseShown ? context.passphraseShown : false
user.language = context.language ? context.language : 'en'
user.disabled = context.disabled ? context.disabled : false
user.groupId = context.groupId ? context.groupId : 1
user.publisherId = context.publisherId ? context.publisherId : 0
return user
})

View File

@ -1,21 +1,31 @@
import Faker from 'faker' import Faker from 'faker'
import { define } from 'typeorm-seeding' import { define } from 'typeorm-seeding'
import { User } from '../../entity/User' import { User } from '../../entity/User'
import { randomBytes } from 'crypto' import { randomBytes, randomInt } from 'crypto'
import { UserContext } from '../interface/UserContext' import { UserContext } from '../interface/UserContext'
define(User, (faker: typeof Faker, context?: UserContext) => { define(User, (faker: typeof Faker, context?: UserContext) => {
if (!context) context = {} if (!context) context = {}
const user = new User() const user = new User()
user.pubkey = context.pubkey ? context.pubkey : randomBytes(32) user.pubKey = context.pubKey ? context.pubKey : randomBytes(32)
user.email = context.email ? context.email : faker.internet.email() user.email = context.email ? context.email : faker.internet.email()
user.firstName = context.firstName ? context.firstName : faker.name.firstName() user.firstName = context.firstName ? context.firstName : faker.name.firstName()
user.lastName = context.lastName ? context.lastName : faker.name.lastName() user.lastName = context.lastName ? context.lastName : faker.name.lastName()
user.username = context.username ? context.username : faker.internet.userName() user.username = context.username ? context.username : faker.internet.userName()
user.disabled = context.disabled ? context.disabled : false user.disabled = context.disabled ? context.disabled : false
user.groupId = 0
user.indexId = 0 user.indexId = 0
user.description = context.description ? context.description : faker.random.words(4)
// TODO Create real password and keys/hash
user.password = context.password ? context.password : BigInt(0)
user.privKey = context.privKey ? context.privKey : randomBytes(80)
user.emailHash = context.emailHash ? context.emailHash : randomBytes(32)
user.createdAt = context.createdAt ? context.createdAt : faker.date.recent()
user.emailChecked = context.emailChecked === undefined ? false : context.emailChecked
user.passphraseShown = context.passphraseShown ? context.passphraseShown : false
user.language = context.language ? context.language : 'en'
user.publisherId = context.publisherId ? context.publisherId : 0
user.passphrase = context.passphrase ? context.passphrase : faker.random.words(24)
return user return user
}) })

View File

@ -1,35 +1,20 @@
export interface UserContext { export interface UserContext {
pubkey?: Buffer pubKey?: Buffer
email?: string email?: string
firstName?: string firstName?: string
lastName?: string lastName?: string
username?: string username?: string
disabled?: boolean disabled?: boolean
}
export interface LoginUserContext {
email?: string
firstName?: string
lastName?: string
username?: string
description?: string description?: string
password?: BigInt password?: BigInt
pubKey?: Buffer
privKey?: Buffer privKey?: Buffer
emailHash?: Buffer emailHash?: Buffer
createdAt?: Date createdAt?: Date
emailChecked?: boolean emailChecked?: boolean
passphraseShown?: boolean passphraseShown?: boolean
language?: string language?: string
disabled?: boolean
groupId?: number
publisherId?: number publisherId?: number
}
export interface LoginUserBackupContext {
userId?: number
passphrase?: string passphrase?: string
mnemonicType?: number
} }
export interface ServerUserContext { export interface ServerUserContext {
@ -42,8 +27,3 @@ export interface ServerUserContext {
created?: Date created?: Date
modified?: Date modified?: Date
} }
export interface LoginUserRolesContext {
userId?: number
roleId?: number
}

View File

@ -1,5 +1,5 @@
export interface UserInterface { export interface UserInterface {
// from login user (contains state user) // from user
email?: string email?: string
firstName?: string firstName?: string
lastName?: string lastName?: string
@ -16,9 +16,7 @@ export interface UserInterface {
disabled?: boolean disabled?: boolean
groupId?: number groupId?: number
publisherId?: number publisherId?: number
// from login user backup
passphrase?: string passphrase?: string
mnemonicType?: number
// from server user // from server user
serverUserPassword?: string serverUserPassword?: string
role?: string role?: string

View File

@ -1,10 +1,4 @@
import { import { UserContext, ServerUserContext } from '../../interface/UserContext'
UserContext,
LoginUserContext,
LoginUserBackupContext,
ServerUserContext,
LoginUserRolesContext,
} from '../../interface/UserContext'
import { import {
BalanceContext, BalanceContext,
TransactionContext, TransactionContext,
@ -13,8 +7,6 @@ import {
} from '../../interface/TransactionContext' } from '../../interface/TransactionContext'
import { UserInterface } from '../../interface/UserInterface' import { UserInterface } from '../../interface/UserInterface'
import { User } from '../../../entity/User' import { User } from '../../../entity/User'
import { LoginUser } from '../../../entity/LoginUser'
import { LoginUserBackup } from '../../../entity/LoginUserBackup'
import { ServerUser } from '../../../entity/ServerUser' import { ServerUser } from '../../../entity/ServerUser'
import { Balance } from '../../../entity/Balance' import { Balance } from '../../../entity/Balance'
import { Transaction } from '../../../entity/Transaction' import { Transaction } from '../../../entity/Transaction'
@ -24,9 +16,6 @@ import { Factory } from 'typeorm-seeding'
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => { export const userSeeder = async (factory: Factory, userData: UserInterface): Promise<void> => {
const user = await factory(User)(createUserContext(userData)).create() const user = await factory(User)(createUserContext(userData)).create()
if (!userData.email) userData.email = user.email
const loginUser = await factory(LoginUser)(createLoginUserContext(userData)).create()
await factory(LoginUserBackup)(createLoginUserBackupContext(userData, loginUser)).create()
if (userData.isAdmin) { if (userData.isAdmin) {
await factory(ServerUser)(createServerUserContext(userData)).create() await factory(ServerUser)(createServerUserContext(userData)).create()
@ -49,47 +38,24 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro
const createUserContext = (context: UserInterface): UserContext => { const createUserContext = (context: UserInterface): UserContext => {
return { return {
pubkey: context.pubKey, pubKey: context.pubKey,
email: context.email, email: context.email,
firstName: context.firstName, firstName: context.firstName,
lastName: context.lastName, lastName: context.lastName,
username: context.username, username: context.username,
disabled: context.disabled, disabled: context.disabled,
}
}
const createLoginUserContext = (context: UserInterface): LoginUserContext => {
return {
email: context.email,
firstName: context.firstName,
lastName: context.lastName,
username: context.username,
description: context.description, description: context.description,
password: context.password, password: context.password,
pubKey: context.pubKey,
privKey: context.privKey, privKey: context.privKey,
emailHash: context.emailHash, emailHash: context.emailHash,
createdAt: context.createdAt, createdAt: context.createdAt,
emailChecked: context.emailChecked, emailChecked: context.emailChecked,
passphraseShown: context.passphraseShown, passphraseShown: context.passphraseShown,
language: context.language, language: context.language,
disabled: context.disabled,
groupId: context.groupId,
publisherId: context.publisherId, publisherId: context.publisherId,
} }
} }
const createLoginUserBackupContext = (
context: UserInterface,
loginUser: LoginUser,
): LoginUserBackupContext => {
return {
passphrase: context.passphrase,
mnemonicType: context.mnemonicType,
userId: loginUser.id,
}
}
const createServerUserContext = (context: UserInterface): ServerUserContext => { const createServerUserContext = (context: UserInterface): ServerUserContext => {
return { return {
role: context.role, role: context.role,
@ -103,13 +69,6 @@ const createServerUserContext = (context: UserInterface): ServerUserContext => {
} }
} }
const createLoginUserRolesContext = (loginUser: LoginUser): LoginUserRolesContext => {
return {
userId: loginUser.id,
roleId: 1,
}
}
const createBalanceContext = (context: UserInterface, user: User): BalanceContext => { const createBalanceContext = (context: UserInterface, user: User): BalanceContext => {
return { return {
modified: context.balanceModified, modified: context.balanceModified,

View File

@ -69,6 +69,7 @@
"memo": "Nachricht", "memo": "Nachricht",
"message": "Nachricht", "message": "Nachricht",
"new_balance": "Neuer Kontostand nach Bestätigung", "new_balance": "Neuer Kontostand nach Bestätigung",
"no_gdd_available": "Du hast keine GDD zum versenden.",
"password": "Passwort", "password": "Passwort",
"passwordRepeat": "Passwort wiederholen", "passwordRepeat": "Passwort wiederholen",
"password_new": "Neues Passwort", "password_new": "Neues Passwort",

View File

@ -69,6 +69,7 @@
"memo": "Message", "memo": "Message",
"message": "Message", "message": "Message",
"new_balance": "Account balance after confirmation", "new_balance": "Account balance after confirmation",
"no_gdd_available": "You do not have GDD to send.",
"password": "Password", "password": "Password",
"passwordRepeat": "Repeat password", "passwordRepeat": "Repeat password",
"password_new": "New password", "password_new": "New password",

View File

@ -21,7 +21,7 @@ describe('GddSend', () => {
} }
const propsData = { const propsData = {
balance: 100.0, balance: 0.0,
} }
const Wrapper = () => { const Wrapper = () => {
@ -37,7 +37,44 @@ describe('GddSend', () => {
expect(wrapper.find('div.transaction-form').exists()).toBeTruthy() expect(wrapper.find('div.transaction-form').exists()).toBeTruthy()
}) })
describe('transaction form disable because balance 0,0 GDD', () => {
it('has a disabled input field of type email', () => {
expect(wrapper.find('#input-group-1').find('input').attributes('disabled')).toBe('disabled')
})
it('has a disabled input field for amount', () => {
expect(wrapper.find('#input-2').find('input').attributes('disabled')).toBe('disabled')
})
it('has a disabled textarea field ', () => {
expect(wrapper.find('#input-3').find('textarea').attributes('disabled')).toBe('disabled')
})
it('has a message indicating that there are no GDDs to send ', () => {
expect(wrapper.find('.text-danger').text()).toBe('form.no_gdd_available')
})
it('has no reset button and no submit button ', () => {
expect(wrapper.find('.test-buttons').exists()).toBeFalsy()
})
})
describe('transaction form', () => { describe('transaction form', () => {
beforeEach(() => {
wrapper.setProps({ balance: 100.0 })
})
describe('transaction form show because balance 100,0 GDD', () => {
it('has no warning message ', () => {
expect(wrapper.find('.text-danger').exists()).toBeFalsy()
})
it('has a reset button', () => {
expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe(
'reset',
)
})
it('has a submit button', () => {
expect(wrapper.find('.test-buttons').findAll('button').at(1).attributes('type')).toBe(
'submit',
)
})
})
describe('email field', () => { describe('email field', () => {
it('has an input field of type email', () => { it('has an input field of type email', () => {
expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email') expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email')

View File

@ -41,6 +41,7 @@
placeholder="E-Mail" placeholder="E-Mail"
style="font-size: large" style="font-size: large"
class="pl-3" class="pl-3"
:disabled="isBalanceDisabled"
></b-form-input> ></b-form-input>
</b-input-group> </b-input-group>
<b-col v-if="errors"> <b-col v-if="errors">
@ -76,6 +77,7 @@
:placeholder="$n(0.01)" :placeholder="$n(0.01)"
style="font-size: large" style="font-size: large"
class="pl-3" class="pl-3"
:disabled="isBalanceDisabled"
></b-form-input> ></b-form-input>
</b-input-group> </b-input-group>
<b-col v-if="errors"> <b-col v-if="errors">
@ -105,6 +107,7 @@
v-model="form.memo" v-model="form.memo"
class="pl-3" class="pl-3"
style="font-size: large" style="font-size: large"
:disabled="isBalanceDisabled"
></b-form-textarea> ></b-form-textarea>
</b-input-group> </b-input-group>
<b-col v-if="errors"> <b-col v-if="errors">
@ -114,7 +117,10 @@
</div> </div>
<br /> <br />
<b-row> <div v-if="!!isBalanceDisabled" class="text-danger">
{{ $t('form.no_gdd_available') }}
</div>
<b-row v-else class="test-buttons">
<b-col> <b-col>
<b-button type="reset" variant="secondary" @click="onReset"> <b-button type="reset" variant="secondary" @click="onReset">
{{ $t('form.reset') }} {{ $t('form.reset') }}
@ -192,6 +198,11 @@ export default {
this.form.email = this.form.email.trim() this.form.email = this.form.email.trim()
}, },
}, },
computed: {
isBalanceDisabled() {
return this.balance <= 0 ? 'disabled' : false
},
},
} }
</script> </script>
<style> <style>