diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d7aff56f3..383735507 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -448,7 +448,7 @@ jobs:
report_name: Coverage Admin Interface
type: lcov
result_path: ./coverage/lcov.info
- min_coverage: 91
+ min_coverage: 93
token: ${{ github.token }}
##############################################################################
diff --git a/admin/src/App.vue b/admin/src/App.vue
index 40460eda4..bcaab2ef9 100644
--- a/admin/src/App.vue
+++ b/admin/src/App.vue
@@ -13,3 +13,11 @@ export default {
components: { defaultLayout },
}
+
diff --git a/admin/src/components/UserTable.spec.js b/admin/src/components/UserTable.spec.js
index 982b65a81..d900b126d 100644
--- a/admin/src/components/UserTable.spec.js
+++ b/admin/src/components/UserTable.spec.js
@@ -11,22 +11,36 @@ describe('UserTable', () => {
const defaultItemsUser = [
{
- email: 'bibi@bloxberg.de',
+ userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
- creation: [1000, 1000, 1000],
+ email: 'bibi@bloxberg.de',
+ creation: [200, 400, 600],
+ emailChecked: true,
},
{
- email: 'bibi@bloxberg.de',
- firstName: 'Bibi',
- lastName: 'Bloxberg',
+ userId: 2,
+ firstName: 'Benjamin',
+ lastName: 'Blümchen',
+ email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
+ emailChecked: true,
},
{
- email: 'bibi@bloxberg.de',
- firstName: 'Bibi',
- lastName: 'Bloxberg',
+ 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,
},
]
@@ -107,7 +121,7 @@ describe('UserTable', () => {
const mocks = {
$t: jest.fn((t) => t),
- $d: jest.fn((d) => d),
+ $d: jest.fn((d) => String(d)),
$apollo: {
query: apolloQueryMock,
},
@@ -122,7 +136,7 @@ describe('UserTable', () => {
describe('mount', () => {
describe('type PageUserSearch', () => {
- beforeEach(() => {
+ beforeEach(async () => {
wrapper = Wrapper(propsDataPageUserSearch)
})
@@ -175,12 +189,12 @@ describe('UserTable', () => {
})
describe('content', () => {
- it('has 3 rows', () => {
- expect(wrapper.findAll('tbody tr').length).toBe(3)
+ it('has 4 rows', () => {
+ expect(wrapper.findAll('tbody tr')).toHaveLength(4)
})
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', () => {
@@ -189,6 +203,110 @@ describe('UserTable', () => {
).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)
+ })
+ })
+ })
})
})
diff --git a/admin/src/components/UserTable.vue b/admin/src/components/UserTable.vue
index 75930c7ba..9292253e3 100644
--- a/admin/src/components/UserTable.vue
+++ b/admin/src/components/UserTable.vue
@@ -141,7 +141,7 @@
variant="danger"
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
size="md"
- @click="overlayShow('remove', row.item)"
+ @click="bookmarkRemove(row.item)"
class="mr-2"
>
@@ -250,13 +250,6 @@ export default {
this.overlayBookmarkType = bookmarkType
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') {
this.overlayText.header = this.$t('overlay.confirm.title')
this.overlayText.text1 = this.$t('overlay.confirm.text')
@@ -266,9 +259,6 @@ export default {
}
},
overlayOK(bookmarkType, item) {
- if (bookmarkType === 'remove') {
- this.bookmarkRemove(item)
- }
if (bookmarkType === 'confirm') {
this.$emit('confirm-creation', item)
}
diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json
index 7585ee122..327cfd302 100644
--- a/admin/src/locales/de.json
+++ b/admin/src/locales/de.json
@@ -54,6 +54,7 @@
}
},
"remove": "Entfernen",
+ "remove_all": "alle Nutzer entfernen",
"transaction": "Transaktion",
"transactionlist": {
"amount": "Betrag",
diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json
index 2680a1c00..3f12f10a8 100644
--- a/admin/src/locales/en.json
+++ b/admin/src/locales/en.json
@@ -54,6 +54,7 @@
}
},
"remove": "Remove",
+ "remove_all": "Remove all users",
"transaction": "Transaction",
"transactionlist": {
"amount": "Amount",
diff --git a/admin/src/pages/Creation.spec.js b/admin/src/pages/Creation.spec.js
index e06e43607..81d556e9b 100644
--- a/admin/src/pages/Creation.spec.js
+++ b/admin/src/pages/Creation.spec.js
@@ -154,55 +154,16 @@ describe('Creation', () => {
},
])
})
- })
- describe('remove item', () => {
- beforeEach(async () => {
- await wrapper
- .findAll('table')
- .at(0)
- .findAll('tbody > tr')
- .at(1)
- .find('button')
- .trigger('click')
- await wrapper
- .findAll('table')
- .at(1)
- .findAll('tbody > tr')
- .at(0)
- .find('button')
- .trigger('click')
- })
-
- it('opens a dialog', () => {
- expect(wrapper.findAll('#overlay').at(1).isVisible()).toBeTruthy()
- })
-
- describe('cancel remove item', () => {
+ describe('remove item', () => {
beforeEach(async () => {
- await wrapper.findAll('#overlay').at(1).findAll('button').at(0).trigger('click')
- })
-
- it('closes the dialog', () => {
- expect(wrapper.findAll('#overlay').at(1).isVisible()).toBeFalsy()
- })
-
- it('has one item in left table', () => {
- expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
- })
-
- it('has one item in right table', () => {
- expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
- })
- })
-
- describe('confirm remove item', () => {
- beforeEach(async () => {
- await wrapper.findAll('#overlay').at(1).findAll('button').at(1).trigger('click')
- })
-
- it('closes the dialog', () => {
- expect(wrapper.findAll('#overlay').at(1).isVisible()).toBeFalsy()
+ await wrapper
+ .findAll('table')
+ .at(1)
+ .findAll('tbody > tr')
+ .at(0)
+ .find('button')
+ .trigger('click')
})
it('has two items in left table', () => {
@@ -223,32 +184,24 @@ describe('Creation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
})
- })
- // this can only happen after API call in CreationForm
- describe('remove all bookmarks', () => {
- beforeEach(async () => {
- await wrapper
- .findAll('table')
- .at(0)
- .findAll('tbody > tr')
- .at(1)
- .find('button')
- .trigger('click')
- jest.clearAllMocks()
- wrapper.findComponent({ name: 'CreationFormular' }).vm.$emit('remove-all-bookmark')
- })
+ describe('remove all bookmarks', () => {
+ beforeEach(async () => {
+ jest.clearAllMocks()
+ await wrapper.find('button.btn-light').trigger('click')
+ })
- it('has no items in right table', () => {
- expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
- })
+ it('has no items in right table', () => {
+ expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
+ })
- it('commits empty array to userSelectedInMassCreation', () => {
- expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
- })
+ it('commits empty array to userSelectedInMassCreation', () => {
+ expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
+ })
- it('calls searchUsers', () => {
- expect(apolloQueryMock).toBeCalled()
+ it('calls searchUsers', () => {
+ expect(apolloQueryMock).toBeCalled()
+ })
})
})
@@ -288,17 +241,38 @@ describe('Creation', () => {
jest.clearAllMocks()
})
- it('calls API when criteria changes', async () => {
- await wrapper.setData({ criteria: 'XX' })
- expect(apolloQueryMock).toBeCalledWith(
- expect.objectContaining({
- variables: {
- searchText: 'XX',
- currentPage: 1,
- pageSize: 25,
- },
- }),
- )
+ describe('search criteria', () => {
+ beforeEach(async () => {
+ await wrapper.setData({ criteria: 'XX' })
+ })
+
+ it('calls API when criteria changes', async () => {
+ expect(apolloQueryMock).toBeCalledWith(
+ 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 () => {
diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue
index 6da762a15..64efab997 100644
--- a/admin/src/pages/Creation.vue
+++ b/admin/src/pages/Creation.vue
@@ -3,12 +3,20 @@
-
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+ {{ $t('remove_all') }}
+
+
+
+
{{ $t('multiple_creation_text') }}
@@ -41,7 +57,7 @@
type="massCreation"
:creation="creation"
:items="itemsMassCreation"
- @remove-all-bookmark="removeAllBookmark"
+ @remove-all-bookmark="removeAllBookmarks"
/>
@@ -123,7 +139,7 @@ export default {
)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
- removeAllBookmark() {
+ removeAllBookmarks() {
this.itemsMassCreation = []
this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers()
diff --git a/admin/src/pages/CreationConfirm.spec.js b/admin/src/pages/CreationConfirm.spec.js
index 5768e1078..2520fd37b 100644
--- a/admin/src/pages/CreationConfirm.spec.js
+++ b/admin/src/pages/CreationConfirm.spec.js
@@ -78,6 +78,7 @@ describe('CreationConfirm', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
+
it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
})
@@ -85,7 +86,7 @@ describe('CreationConfirm', () => {
describe('remove creation with success', () => {
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', () => {
@@ -107,7 +108,7 @@ describe('CreationConfirm', () => {
describe('remove creation with error', () => {
beforeEach(async () => {
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', () => {
@@ -118,22 +119,52 @@ describe('CreationConfirm', () => {
describe('confirm creation with success', () => {
beforeEach(async () => {
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', () => {
- expect(apolloMutateMock).toBeCalledWith({
- mutation: confirmPendingCreation,
- variables: { id: 2 },
+ describe('overlay', () => {
+ it('opens the overlay', () => {
+ expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
})
- })
- it('commits openCreationsMinus to store', () => {
- expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
- })
+ describe('cancel confirmation', () => {
+ beforeEach(async () => {
+ await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
+ })
- it('toasts a success message', () => {
- expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_created')
+ it('closes the overlay', () => {
+ 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)
+ })
+ })
})
})
diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js
index 64783e82e..fe7bde0cc 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -88,100 +88,6 @@ describe('UserSearch', () => {
)
})
- describe('row toggling', () => {
- it('has 4 users in the table', () => {
- expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
- })
-
- 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', () => {
- it('opens the details', async () => {
- await wrapper
- .findAll('tbody > tr')
- .at(3)
- .findAll('td')
- .at(5)
- .find('button')
- .trigger('click')
- 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',
- )
- })
-
- describe('click on envelope again', () => {
- it('closes the details', async () => {
- await wrapper
- .findAll('tbody > tr')
- .at(3)
- .findAll('td')
- .at(5)
- .find('button')
- .trigger('click')
- expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
- })
- })
-
- describe('click on close details', () => {
- it('closes the details', async () => {
- await wrapper
- .findAll('tbody > tr')
- .at(3)
- .findAll('td')
- .at(5)
- .find('button')
- .trigger('click')
- await wrapper.findAll('tbody > tr').at(5).findAll('button').at(1).trigger('click')
- 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)
- })
- })
- })
-
describe('unconfirmed emails', () => {
beforeEach(async () => {
await wrapper.find('button.btn-block').trigger('click')
@@ -237,6 +143,23 @@ describe('UserSearch', () => {
}),
)
})
+
+ 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,
+ },
+ }),
+ )
+ })
+ })
})
describe('apollo returns error', () => {
diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue
index d4dbd681e..2efe9f8e8 100644
--- a/admin/src/pages/UserSearch.vue
+++ b/admin/src/pages/UserSearch.vue
@@ -7,12 +7,21 @@
-
+
+
+
+
+
+
+
+
+
+
{
- const repository = getCustomRepository(LoginUserRepository)
- const user = await repository.findByEmail(email)
+ const user = await dbUser.findOne({ email })
return user ? user.emailChecked : false
}
diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts
index 61f590123..4bf4e7a17 100644
--- a/backend/src/graphql/resolver/TransactionResolver.ts
+++ b/backend/src/graphql/resolver/TransactionResolver.ts
@@ -33,7 +33,6 @@ import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
import { TransactionTypeId } from '../enum/TransactionTypeId'
import { TransactionType } from '../enum/TransactionType'
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
-import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { RIGHTS } from '../../auth/RIGHTS'
// Helper function
@@ -290,14 +289,13 @@ async function addUserTransaction(
}
async function getPublicKey(email: string): Promise {
- const loginUserRepository = getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findOne({ email: email })
+ const user = await dbUser.findOne({ email: email })
// User not found
- if (!loginUser) {
+ if (!user) {
return null
}
- return loginUser.pubKey.toString('hex')
+ return user.pubKey.toString('hex')
}
@Resolver()
@@ -364,7 +362,7 @@ export class TransactionResolver {
// validate sender user (logged in)
const userRepository = getCustomRepository(UserRepository)
const senderUser = await userRepository.findByPubkeyHex(context.pubKey)
- if (senderUser.pubkey.length !== 32) {
+ if (senderUser.pubKey.length !== 32) {
throw new Error('invalid sender public key')
}
if (!hasUserAmount(senderUser, amount)) {
@@ -454,7 +452,7 @@ export class TransactionResolver {
const transactionSendCoin = new dbTransactionSendCoin()
transactionSendCoin.transactionId = transaction.id
transactionSendCoin.userId = senderUser.id
- transactionSendCoin.senderPublic = senderUser.pubkey
+ transactionSendCoin.senderPublic = senderUser.pubKey
transactionSendCoin.recipiantUserId = recipiantUser.id
transactionSendCoin.recipiantPublic = Buffer.from(recipiantPublicKey, 'hex')
transactionSendCoin.amount = centAmount
diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts
index 14a56b60b..d7ecfa797 100644
--- a/backend/src/graphql/resolver/UserResolver.ts
+++ b/backend/src/graphql/resolver/UserResolver.ts
@@ -14,11 +14,8 @@ import UnsecureLoginArgs from '../arg/UnsecureLoginArgs'
import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs'
import { klicktippNewsletterStateMiddleware } from '../../middleware/klicktippMiddleware'
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
-import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
import { Setting } from '../enum/Setting'
import { UserRepository } from '../../typeorm/repository/User'
-import { LoginUser } from '@entity/LoginUser'
-import { LoginUserBackup } from '@entity/LoginUserBackup'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
@@ -27,7 +24,6 @@ import { klicktippSignIn } from '../../apis/KlicktippController'
import { RIGHTS } from '../../auth/RIGHTS'
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
import { ROLE_ADMIN } from '../../auth/ROLES'
-import { randomBytes } from 'crypto'
const EMAIL_OPT_IN_RESET_PASSWORD = 2
const EMAIL_OPT_IN_REGISTER = 1
@@ -186,10 +182,10 @@ const createEmailOptIn = async (
return emailOptIn
}
-const getOptInCode = async (loginUser: LoginUser): Promise => {
+const getOptInCode = async (loginUserId: number): Promise => {
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
let optInCode = await loginEmailOptInRepository.findOne({
- userId: loginUser.id,
+ userId: loginUserId,
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
})
@@ -207,7 +203,7 @@ const getOptInCode = async (loginUser: LoginUser): Promise => {
} else {
optInCode = new LoginEmailOptIn()
optInCode.verificationCode = random(64)
- optInCode.userId = loginUser.id
+ optInCode.userId = loginUserId
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
}
await loginEmailOptInRepository.save(optInCode)
@@ -223,17 +219,15 @@ export class UserResolver {
// TODO refactor and do not have duplicate code with login(see below)
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
- const loginUserRepository = getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findByEmail(userEntity.email)
const user = new User()
user.id = userEntity.id
user.email = userEntity.email
user.firstName = userEntity.firstName
user.lastName = userEntity.lastName
user.username = userEntity.username
- user.description = loginUser.description
- user.pubkey = userEntity.pubkey.toString('hex')
- user.language = loginUser.language
+ user.description = userEntity.description
+ user.pubkey = userEntity.pubKey.toString('hex')
+ user.language = userEntity.language
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context)
@@ -259,76 +253,50 @@ export class UserResolver {
@Ctx() context: any,
): Promise {
email = email.trim().toLowerCase()
- const loginUserRepository = getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findByEmail(email).catch(() => {
+ const dbUser = await DbUser.findOneOrFail({ email }).catch(() => {
throw new Error('No user with this credentials')
})
- if (!loginUser.emailChecked) {
+ if (!dbUser.emailChecked) {
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
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
throw new Error('User has no private or publicKey')
}
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()) {
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()
- user.id = userEntity.id
+ user.id = dbUser.id
user.email = email
- user.firstName = loginUser.firstName
- user.lastName = loginUser.lastName
- user.username = loginUser.username
- user.description = loginUser.description
- user.pubkey = loginUserPubKeyString
- user.language = loginUser.language
+ user.firstName = dbUser.firstName
+ user.lastName = dbUser.lastName
+ user.username = dbUser.username
+ user.description = dbUser.description
+ user.pubkey = dbUser.pubKey.toString('hex')
+ user.language = dbUser.language
// 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) {
user.publisherId = publisherId
// TODO: Check if we can use updateUserInfos
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
- const loginUserRepository = getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
- loginUser.publisherId = publisherId
- loginUserRepository.save(loginUser)
+ dbUser.publisherId = publisherId
+ DbUser.save(dbUser)
}
// coinAnimation
const userSettingRepository = getCustomRepository(UserSettingRepository)
const coinanimation = await userSettingRepository
- .readBoolean(userEntity.id, Setting.COIN_ANIMATION)
+ .readBoolean(dbUser.id, Setting.COIN_ANIMATION)
.catch((error) => {
throw new Error(error)
})
@@ -341,7 +309,7 @@ export class UserResolver {
context.setHeaders.push({
key: 'token',
- value: encode(loginUser.pubKey),
+ value: encode(dbUser.pubKey),
})
return user
@@ -393,18 +361,21 @@ export class UserResolver {
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
const emailHash = getEmailHash(email)
- // Table: login_users
- const loginUser = new LoginUser()
- loginUser.email = email
- loginUser.firstName = firstName
- loginUser.lastName = lastName
- loginUser.username = username
- loginUser.description = ''
+ // Table: state_users
+ const dbUser = new DbUser()
+ dbUser.email = email
+ dbUser.firstName = firstName
+ dbUser.lastName = lastName
+ dbUser.username = username
+ 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.emailHash = emailHash
- loginUser.language = language
- loginUser.groupId = 1
- loginUser.publisherId = publisherId
// loginUser.pubKey = keyPair[0]
// loginUser.privKey = encryptedPrivkey
@@ -412,43 +383,15 @@ export class UserResolver {
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
- const { id: loginUserId } = await queryRunner.manager.save(loginUser).catch((error) => {
+ await queryRunner.manager.save(dbUser).catch((error) => {
// eslint-disable-next-line no-console
- console.log('insert LoginUser failed', 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)
+ console.log('Error while saving dbUser', error)
throw new Error('error saving user')
})
// Store EmailOptIn in DB
// 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(
/{code}/g,
@@ -480,15 +423,14 @@ export class UserResolver {
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@Mutation(() => Boolean)
async sendActivationEmail(@Arg('email') email: string): Promise {
- const loginUserRepository = getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findOneOrFail({ email: email })
+ const user = await DbUser.findOneOrFail({ email: email })
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
- const emailOptIn = await createEmailOptIn(loginUser.id, queryRunner)
+ const emailOptIn = await createEmailOptIn(user.id, queryRunner)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{code}/g,
@@ -497,8 +439,8 @@ export class UserResolver {
const emailSent = await sendAccountActivationEmail({
link: activationLink,
- firstName: loginUser.firstName,
- lastName: loginUser.lastName,
+ firstName: user.firstName,
+ lastName: user.lastName,
email,
})
@@ -522,10 +464,9 @@ export class UserResolver {
async sendResetPasswordEmail(@Arg('email') email: string): Promise {
// TODO: this has duplicate code with createUser
- const loginUserRepository = await getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findOneOrFail({ email })
+ const user = await DbUser.findOneOrFail({ email })
- const optInCode = await getOptInCode(loginUser)
+ const optInCode = await getOptInCode(user.id)
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
/{code}/g,
@@ -534,8 +475,8 @@ export class UserResolver {
const emailSent = await sendResetPasswordEmail({
link,
- firstName: loginUser.firstName,
- lastName: loginUser.lastName,
+ firstName: user.firstName,
+ lastName: user.lastName,
email,
})
@@ -575,34 +516,18 @@ export class UserResolver {
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
- const dbUserRepository = await getCustomRepository(UserRepository)
- const dbUser = await dbUserRepository.findOneOrFail({ email: loginUser.email }).catch(() => {
- throw new Error('Could not find corresponding User')
+ const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
+ throw new Error('Could not find corresponding Login User')
})
- const loginUserBackupRepository = await getRepository(LoginUserBackup)
- let loginUserBackup = await loginUserBackupRepository.findOne({ userId: loginUser.id })
-
// Generate Passphrase if needed
- if (!loginUserBackup) {
+ if (!user.passphrase) {
const passphrase = PassphraseGenerate()
- loginUserBackup = new LoginUserBackup()
- loginUserBackup.userId = loginUser.id
- loginUserBackup.passphrase = passphrase.join(' ') // login server saves trailing space
- loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
- loginUserBackupRepository.save(loginUserBackup)
+ user.passphrase = passphrase.join(' ')
}
- const passphrase = loginUserBackup.passphrase.split(' ')
+ const passphrase = user.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) {
// TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont
@@ -611,29 +536,23 @@ export class UserResolver {
}
// Activate EMail
- loginUser.emailChecked = true
+ user.emailChecked = true
// 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 encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
- loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
- loginUser.pubKey = keyPair[0]
- loginUser.privKey = encryptedPrivkey
- dbUser.pubkey = keyPair[0]
+ user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
+ user.pubKey = keyPair[0]
+ user.privKey = encryptedPrivkey
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
- // Save loginUser
- await queryRunner.manager.save(loginUser).catch((error) => {
- throw new Error('error saving loginUser: ' + error)
- })
-
// Save user
- await queryRunner.manager.save(dbUser).catch((error) => {
+ await queryRunner.manager.save(user).catch((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?
if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) {
try {
- await klicktippSignIn(
- loginUser.email,
- loginUser.language,
- loginUser.firstName,
- loginUser.lastName,
- )
+ await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
} catch {
// TODO is this a problem?
// eslint-disable-next-line no-console
@@ -689,8 +603,6 @@ export class UserResolver {
): Promise {
const userRepository = getCustomRepository(UserRepository)
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
- const loginUserRepository = getCustomRepository(LoginUserRepository)
- const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
if (username) {
throw new Error('change username currently not supported!')
@@ -704,46 +616,44 @@ export class UserResolver {
}
if (firstName) {
- loginUser.firstName = firstName
userEntity.firstName = firstName
}
if (lastName) {
- loginUser.lastName = lastName
userEntity.lastName = lastName
}
if (description) {
- loginUser.description = description
+ userEntity.description = description
}
if (language) {
if (!isLanguage(language)) {
throw new Error(`"${language}" isn't a valid language`)
}
- loginUser.language = language
+ userEntity.language = language
}
if (password && passwordNew) {
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
- const oldPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, password)
- if (BigInt(loginUser.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
+ const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
+ if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
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])
// Save new password hash and newly encrypted private key
- loginUser.password = newPasswordHash[0].readBigUInt64LE()
- loginUser.privKey = encryptedPrivkey
+ userEntity.password = newPasswordHash[0].readBigUInt64LE()
+ userEntity.privKey = encryptedPrivkey
}
// Save publisherId only if Elopage is not yet registered
if (publisherId && !(await this.hasElopage(context))) {
- loginUser.publisherId = publisherId
+ userEntity.publisherId = publisherId
}
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) => {
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.`)
}
- const usersFound = await LoginUser.count({ username })
+ const usersFound = await DbUser.count({ username })
// Username already present?
if (usersFound !== 0) {
diff --git a/backend/src/typeorm/repository/LoginUser.ts b/backend/src/typeorm/repository/LoginUser.ts
deleted file mode 100644
index efe6f5428..000000000
--- a/backend/src/typeorm/repository/LoginUser.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { EntityRepository, Repository } from '@dbTools/typeorm'
-import { LoginUser } from '@entity/LoginUser'
-
-@EntityRepository(LoginUser)
-export class LoginUserRepository extends Repository {
- async findByEmail(email: string): Promise {
- return this.createQueryBuilder('loginUser')
- .where('loginUser.email = :email', { email })
- .getOneOrFail()
- }
-
- async findBySearchCriteria(searchCriteria: string): Promise {
- 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()
- }
-}
diff --git a/backend/src/typeorm/repository/LoginUserBackup.ts b/backend/src/typeorm/repository/LoginUserBackup.ts
deleted file mode 100644
index a54b1e8af..000000000
--- a/backend/src/typeorm/repository/LoginUserBackup.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { EntityRepository, Repository } from '@dbTools/typeorm'
-import { LoginUserBackup } from '@entity/LoginUserBackup'
-
-@EntityRepository(LoginUserBackup)
-export class LoginUserBackupRepository extends Repository {}
diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts
index fa7115429..59d6ff465 100644
--- a/backend/src/typeorm/repository/User.ts
+++ b/backend/src/typeorm/repository/User.ts
@@ -5,7 +5,7 @@ import { User } from '@entity/User'
export class UserRepository extends Repository {
async findByPubkeyHex(pubkeyHex: string): Promise {
return this.createQueryBuilder('user')
- .where('hex(user.pubkey) = :pubkeyHex', { pubkeyHex })
+ .where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
.getOneOrFail()
}
diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts
index 9c1aadd2e..0b392abb1 100644
--- a/backend/src/webhook/elopage.ts
+++ b/backend/src/webhook/elopage.ts
@@ -31,7 +31,7 @@ import { LoginElopageBuys } from '@entity/LoginElopageBuys'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserResolver } from '../graphql/resolver/UserResolver'
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 => {
// eslint-disable-next-line no-console
@@ -114,8 +114,7 @@ export const elopageWebhook = async (req: any, res: any): Promise => {
}
// Do we already have such a user?
- const loginUserRepository = await getCustomRepository(LoginUserRepository)
- if ((await loginUserRepository.count({ email })) !== 0) {
+ if ((await dbUser.count({ email })) !== 0) {
// eslint-disable-next-line no-console
console.log(`Did not create User - already exists with email: ${email}`)
return
diff --git a/database/entity/0001-init_db/User.ts b/database/entity/0001-init_db/User.ts
index b349e2584..be2c4c5ad 100644
--- a/database/entity/0001-init_db/User.ts
+++ b/database/entity/0001-init_db/User.ts
@@ -7,13 +7,13 @@ export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
- @Column({ name: 'index_id', default: 0 })
+ @Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number
@Column({ name: 'group_id', default: 0, unsigned: true })
groupId: number
- @Column({ type: 'binary', length: 32, name: 'public_key' })
+ @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubkey: Buffer
@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' })
username: string
- @Column()
+ @Column({ type: 'bool', default: false })
disabled: boolean
@OneToOne(() => Balance, (balance) => balance.user)
diff --git a/database/entity/0002-add_settings/User.ts b/database/entity/0002-add_settings/User.ts
index 40f5d400a..a756cbbd5 100644
--- a/database/entity/0002-add_settings/User.ts
+++ b/database/entity/0002-add_settings/User.ts
@@ -7,13 +7,13 @@ export class User extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
- @Column({ name: 'index_id', default: 0 })
+ @Column({ name: 'index_id', type: 'smallint', default: 0, nullable: false })
indexId: number
@Column({ name: 'group_id', default: 0, unsigned: true })
groupId: number
- @Column({ type: 'binary', length: 32, name: 'public_key' })
+ @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
pubkey: Buffer
@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' })
username: string
- @Column()
+ @Column({ type: 'bool', default: false })
disabled: boolean
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
diff --git a/database/entity/0003-login_server_tables/LoginUser.ts b/database/entity/0003-login_server_tables/LoginUser.ts
index 07816254f..a3a83f450 100644
--- a/database/entity/0003-login_server_tables/LoginUser.ts
+++ b/database/entity/0003-login_server_tables/LoginUser.ts
@@ -1,5 +1,5 @@
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
@Entity('login_users')
@@ -19,7 +19,7 @@ export class LoginUser extends BaseEntity {
@Column({ length: 255, default: '' })
username: string
- @Column({ default: '', nullable: true })
+ @Column({ type: 'mediumtext', default: '', nullable: true })
description: string
@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 })
emailHash: Buffer
- @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
+ @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
- @Column({ name: 'email_checked', default: 0 })
+ @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
- @Column({ name: 'passphrase_shown', default: 0 })
+ @Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean
- @Column({ length: 4, default: 'de' })
+ @Column({ length: 4, default: 'de', nullable: false })
language: string
- @Column({ default: 0 })
+ @Column({ type: 'bool', default: false })
disabled: boolean
@Column({ name: 'group_id', default: 0, unsigned: true })
diff --git a/database/entity/0003-login_server_tables/LoginUserBackup.ts b/database/entity/0003-login_server_tables/LoginUserBackup.ts
index 7152e12e5..39f5e0db5 100644
--- a/database/entity/0003-login_server_tables/LoginUserBackup.ts
+++ b/database/entity/0003-login_server_tables/LoginUserBackup.ts
@@ -1,5 +1,5 @@
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, JoinColumn, OneToOne } from 'typeorm'
-import { LoginUser } from '../LoginUser'
+import { LoginUser } from './LoginUser'
@Entity('login_user_backups')
export class LoginUserBackup extends BaseEntity {
diff --git a/database/entity/0006-login_users_collation/LoginUser.ts b/database/entity/0006-login_users_collation/LoginUser.ts
index e12c82e27..fdb17f4ad 100644
--- a/database/entity/0006-login_users_collation/LoginUser.ts
+++ b/database/entity/0006-login_users_collation/LoginUser.ts
@@ -1,5 +1,5 @@
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
@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' })
username: string
- @Column({ default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
+ @Column({ type: 'mediumtext', default: '', collation: 'utf8mb4_unicode_ci', nullable: true })
description: string
@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 })
emailHash: Buffer
- @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP' })
+ @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
createdAt: Date
- @Column({ name: 'email_checked', default: 0 })
+ @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
emailChecked: boolean
- @Column({ name: 'passphrase_shown', default: 0 })
+ @Column({ name: 'passphrase_shown', type: 'bool', nullable: false, default: false })
passphraseShown: boolean
- @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci' })
+ @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
language: string
- @Column({ default: 0 })
+ @Column({ type: 'bool', default: false })
disabled: boolean
@Column({ name: 'group_id', default: 0, unsigned: true })
diff --git a/database/entity/0017-combine_user_tables/LoginUserBackup.ts b/database/entity/0017-combine_user_tables/LoginUserBackup.ts
new file mode 100644
index 000000000..7aa69a021
--- /dev/null
+++ b/database/entity/0017-combine_user_tables/LoginUserBackup.ts
@@ -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
+}
diff --git a/database/entity/0017-combine_user_tables/User.ts b/database/entity/0017-combine_user_tables/User.ts
new file mode 100644
index 000000000..a9bf29d24
--- /dev/null
+++ b/database/entity/0017-combine_user_tables/User.ts
@@ -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[]
+}
diff --git a/database/entity/0018-combine_login_user_backups_and_user_table/User.ts b/database/entity/0018-combine_login_user_backups_and_user_table/User.ts
new file mode 100644
index 000000000..2ae351e47
--- /dev/null
+++ b/database/entity/0018-combine_login_user_backups_and_user_table/User.ts
@@ -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[]
+}
diff --git a/database/entity/0019-replace_login_user_id_with_state_user_id/User.ts b/database/entity/0019-replace_login_user_id_with_state_user_id/User.ts
new file mode 100644
index 000000000..b469a55a7
--- /dev/null
+++ b/database/entity/0019-replace_login_user_id_with_state_user_id/User.ts
@@ -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[]
+}
diff --git a/database/entity/LoginUser.ts b/database/entity/LoginUser.ts
deleted file mode 100644
index b22e1137f..000000000
--- a/database/entity/LoginUser.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { LoginUser } from './0006-login_users_collation/LoginUser'
diff --git a/database/entity/LoginUserBackup.ts b/database/entity/LoginUserBackup.ts
deleted file mode 100644
index 23d2c9271..000000000
--- a/database/entity/LoginUserBackup.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { LoginUserBackup } from './0003-login_server_tables/LoginUserBackup'
diff --git a/database/entity/User.ts b/database/entity/User.ts
index b20e934f1..6dcdfed68 100644
--- a/database/entity/User.ts
+++ b/database/entity/User.ts
@@ -1 +1 @@
-export { User } from './0002-add_settings/User'
+export { User } from './0019-replace_login_user_id_with_state_user_id/User'
diff --git a/database/entity/index.ts b/database/entity/index.ts
index cd1dd4e21..37fe6eb55 100644
--- a/database/entity/index.ts
+++ b/database/entity/index.ts
@@ -1,8 +1,6 @@
import { Balance } from './Balance'
import { LoginElopageBuys } from './LoginElopageBuys'
import { LoginEmailOptIn } from './LoginEmailOptIn'
-import { LoginUser } from './LoginUser'
-import { LoginUserBackup } from './LoginUserBackup'
import { Migration } from './Migration'
import { ServerUser } from './ServerUser'
import { Transaction } from './Transaction'
@@ -18,8 +16,6 @@ export const entities = [
Balance,
LoginElopageBuys,
LoginEmailOptIn,
- LoginUser,
- LoginUserBackup,
Migration,
ServerUser,
Transaction,
diff --git a/database/migrations/0017-combine_user_tables.ts b/database/migrations/0017-combine_user_tables.ts
new file mode 100644
index 000000000..04be53615
--- /dev/null
+++ b/database/migrations/0017-combine_user_tables.ts
@@ -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>) {
+ // 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>) {
+ 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;',
+ )
+}
diff --git a/database/migrations/0018-combine_login_user_backups_and_user_table.ts b/database/migrations/0018-combine_login_user_backups_and_user_table.ts
new file mode 100644
index 000000000..2141017bd
--- /dev/null
+++ b/database/migrations/0018-combine_login_user_backups_and_user_table.ts
@@ -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>) {
+ // 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>) {
+ 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`;')
+}
diff --git a/database/migrations/0019-replace_login_user_id_with_state_user_id.ts b/database/migrations/0019-replace_login_user_id_with_state_user_id.ts
new file mode 100644
index 000000000..719c05443
--- /dev/null
+++ b/database/migrations/0019-replace_login_user_id_with_state_user_id.ts
@@ -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>) {
+ // 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>) {
+ 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');
+ `)
+}
diff --git a/database/src/factories/login-user-backup.factory.ts b/database/src/factories/login-user-backup.factory.ts
deleted file mode 100644
index c4ae18a77..000000000
--- a/database/src/factories/login-user-backup.factory.ts
+++ /dev/null
@@ -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
-})
diff --git a/database/src/factories/login-user.factory.ts b/database/src/factories/login-user.factory.ts
deleted file mode 100644
index b3c0312f3..000000000
--- a/database/src/factories/login-user.factory.ts
+++ /dev/null
@@ -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
-})
diff --git a/database/src/factories/user.factory.ts b/database/src/factories/user.factory.ts
index 1f684f23f..966e5ffc8 100644
--- a/database/src/factories/user.factory.ts
+++ b/database/src/factories/user.factory.ts
@@ -1,21 +1,31 @@
import Faker from 'faker'
import { define } from 'typeorm-seeding'
import { User } from '../../entity/User'
-import { randomBytes } from 'crypto'
+import { randomBytes, randomInt } from 'crypto'
import { UserContext } from '../interface/UserContext'
define(User, (faker: typeof Faker, context?: UserContext) => {
if (!context) context = {}
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.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.disabled = context.disabled ? context.disabled : false
- user.groupId = 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
})
diff --git a/database/src/interface/UserContext.ts b/database/src/interface/UserContext.ts
index eb4323aee..0fa1a61b5 100644
--- a/database/src/interface/UserContext.ts
+++ b/database/src/interface/UserContext.ts
@@ -1,35 +1,20 @@
export interface UserContext {
- pubkey?: Buffer
+ pubKey?: Buffer
email?: string
firstName?: string
lastName?: string
username?: string
disabled?: boolean
-}
-
-export interface LoginUserContext {
- email?: string
- firstName?: string
- lastName?: string
- username?: string
description?: string
password?: BigInt
- pubKey?: Buffer
privKey?: Buffer
emailHash?: Buffer
createdAt?: Date
emailChecked?: boolean
passphraseShown?: boolean
language?: string
- disabled?: boolean
- groupId?: number
publisherId?: number
-}
-
-export interface LoginUserBackupContext {
- userId?: number
passphrase?: string
- mnemonicType?: number
}
export interface ServerUserContext {
@@ -42,8 +27,3 @@ export interface ServerUserContext {
created?: Date
modified?: Date
}
-
-export interface LoginUserRolesContext {
- userId?: number
- roleId?: number
-}
diff --git a/database/src/interface/UserInterface.ts b/database/src/interface/UserInterface.ts
index 63804af6b..2e20b857f 100644
--- a/database/src/interface/UserInterface.ts
+++ b/database/src/interface/UserInterface.ts
@@ -1,5 +1,5 @@
export interface UserInterface {
- // from login user (contains state user)
+ // from user
email?: string
firstName?: string
lastName?: string
@@ -16,9 +16,7 @@ export interface UserInterface {
disabled?: boolean
groupId?: number
publisherId?: number
- // from login user backup
passphrase?: string
- mnemonicType?: number
// from server user
serverUserPassword?: string
role?: string
diff --git a/database/src/seeds/helpers/user-helpers.ts b/database/src/seeds/helpers/user-helpers.ts
index f205ccb00..55ab40e9d 100644
--- a/database/src/seeds/helpers/user-helpers.ts
+++ b/database/src/seeds/helpers/user-helpers.ts
@@ -1,10 +1,4 @@
-import {
- UserContext,
- LoginUserContext,
- LoginUserBackupContext,
- ServerUserContext,
- LoginUserRolesContext,
-} from '../../interface/UserContext'
+import { UserContext, ServerUserContext } from '../../interface/UserContext'
import {
BalanceContext,
TransactionContext,
@@ -13,8 +7,6 @@ import {
} from '../../interface/TransactionContext'
import { UserInterface } from '../../interface/UserInterface'
import { User } from '../../../entity/User'
-import { LoginUser } from '../../../entity/LoginUser'
-import { LoginUserBackup } from '../../../entity/LoginUserBackup'
import { ServerUser } from '../../../entity/ServerUser'
import { Balance } from '../../../entity/Balance'
import { Transaction } from '../../../entity/Transaction'
@@ -24,9 +16,6 @@ import { Factory } from 'typeorm-seeding'
export const userSeeder = async (factory: Factory, userData: UserInterface): Promise => {
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) {
await factory(ServerUser)(createServerUserContext(userData)).create()
@@ -49,47 +38,24 @@ export const userSeeder = async (factory: Factory, userData: UserInterface): Pro
const createUserContext = (context: UserInterface): UserContext => {
return {
- pubkey: context.pubKey,
+ pubKey: context.pubKey,
email: context.email,
firstName: context.firstName,
lastName: context.lastName,
username: context.username,
disabled: context.disabled,
- }
-}
-
-const createLoginUserContext = (context: UserInterface): LoginUserContext => {
- return {
- email: context.email,
- firstName: context.firstName,
- lastName: context.lastName,
- username: context.username,
description: context.description,
password: context.password,
- pubKey: context.pubKey,
privKey: context.privKey,
emailHash: context.emailHash,
createdAt: context.createdAt,
emailChecked: context.emailChecked,
passphraseShown: context.passphraseShown,
language: context.language,
- disabled: context.disabled,
- groupId: context.groupId,
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 => {
return {
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 => {
return {
modified: context.balanceModified,
diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json
index d92f42f5d..fbd9c8e91 100644
--- a/frontend/src/locales/de.json
+++ b/frontend/src/locales/de.json
@@ -69,6 +69,7 @@
"memo": "Nachricht",
"message": "Nachricht",
"new_balance": "Neuer Kontostand nach Bestätigung",
+ "no_gdd_available": "Du hast keine GDD zum versenden.",
"password": "Passwort",
"passwordRepeat": "Passwort wiederholen",
"password_new": "Neues Passwort",
diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json
index 014d449a0..534aa25e8 100644
--- a/frontend/src/locales/en.json
+++ b/frontend/src/locales/en.json
@@ -69,6 +69,7 @@
"memo": "Message",
"message": "Message",
"new_balance": "Account balance after confirmation",
+ "no_gdd_available": "You do not have GDD to send.",
"password": "Password",
"passwordRepeat": "Repeat password",
"password_new": "New password",
diff --git a/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js b/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js
index 463613449..25683e6df 100644
--- a/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js
+++ b/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.spec.js
@@ -21,7 +21,7 @@ describe('GddSend', () => {
}
const propsData = {
- balance: 100.0,
+ balance: 0.0,
}
const Wrapper = () => {
@@ -37,7 +37,44 @@ describe('GddSend', () => {
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', () => {
+ 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', () => {
it('has an input field of type email', () => {
expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email')
diff --git a/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.vue b/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.vue
index 364f54ac7..0f5650543 100644
--- a/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.vue
+++ b/frontend/src/views/Pages/SendOverview/GddSend/TransactionForm.vue
@@ -41,6 +41,7 @@
placeholder="E-Mail"
style="font-size: large"
class="pl-3"
+ :disabled="isBalanceDisabled"
>
@@ -76,6 +77,7 @@
:placeholder="$n(0.01)"
style="font-size: large"
class="pl-3"
+ :disabled="isBalanceDisabled"
>
@@ -105,6 +107,7 @@
v-model="form.memo"
class="pl-3"
style="font-size: large"
+ :disabled="isBalanceDisabled"
>
@@ -114,7 +117,10 @@
-
+
+ {{ $t('form.no_gdd_available') }}
+
+
{{ $t('form.reset') }}
@@ -192,6 +198,11 @@ export default {
this.form.email = this.form.email.trim()
},
},
+ computed: {
+ isBalanceDisabled() {
+ return this.balance <= 0 ? 'disabled' : false
+ },
+ },
}