diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ee602a343..999863dd9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -528,7 +528,7 @@ jobs:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
- min_coverage: 55
+ min_coverage: 65
token: ${{ github.token }}
##########################################################################
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 53376946c..7e8e748af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,8 +4,68 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
+#### [1.8.1](https://github.com/gradido/gradido/compare/1.8.0...1.8.1)
+
+- 1851 integrate and test the behaviour of clipboard polyfill [`#1853`](https://github.com/gradido/gradido/pull/1853)
+- fix: Deprecated Warning from Faker on Seeding [`#1854`](https://github.com/gradido/gradido/pull/1854)
+- feat: Test Admin Resolver [`#1848`](https://github.com/gradido/gradido/pull/1848)
+- devops: Disable DB Reset on Stage 1 [`#1852`](https://github.com/gradido/gradido/pull/1852)
+- 🍰 Refactor notActivated and isDeleted [`#1791`](https://github.com/gradido/gradido/pull/1791)
+- devops: Disable Send Email on Seeding [`#1849`](https://github.com/gradido/gradido/pull/1849)
+- fix: Confirm Creation with Decimal [`#1838`](https://github.com/gradido/gradido/pull/1838)
+- error message by no navigator.clipbord function [`#1841`](https://github.com/gradido/gradido/pull/1841)
+
+#### [1.8.0](https://github.com/gradido/gradido/compare/1.7.1...1.8.0)
+
+> 25 April 2022
+
+- v1.8.0 [`#1836`](https://github.com/gradido/gradido/pull/1836)
+- Fix: database version requirement for backend corrected [`#1835`](https://github.com/gradido/gradido/pull/1835)
+- feat: More User Resolver Tests [`#1827`](https://github.com/gradido/gradido/pull/1827)
+- fix: Round Decay with Tranasction Links [`#1834`](https://github.com/gradido/gradido/pull/1834)
+- Fix: config value for the redeem URL was missing [`#1828`](https://github.com/gradido/gradido/pull/1828)
+- Refactor: Database admin pending creations use decimal [`#1748`](https://github.com/gradido/gradido/pull/1748)
+- refactor: Drop Server User Table [`#1808`](https://github.com/gradido/gradido/pull/1808)
+- 1816 expired link are not highlighted [`#1821`](https://github.com/gradido/gradido/pull/1821)
+- 1812 put qr code into popup on generate [`#1820`](https://github.com/gradido/gradido/pull/1820)
+- Docu: Federation image [`#1817`](https://github.com/gradido/gradido/pull/1817)
+- 1813 qr code popup [`#1819`](https://github.com/gradido/gradido/pull/1819)
+- Fix: cross-env for windows [`#1822`](https://github.com/gradido/gradido/pull/1822)
+- fix: Double Load Transaction Links [`#1818`](https://github.com/gradido/gradido/pull/1818)
+- Generated link in backend should also give back the base url [`#1745`](https://github.com/gradido/gradido/pull/1745)
+- 1731 style startDecayStartblock, style Adapted across pages [`#1809`](https://github.com/gradido/gradido/pull/1809)
+- Refactor: Frontend bake in community info [`#1750`](https://github.com/gradido/gradido/pull/1750)
+- fix: Load Transaction Link Details on Click [`#1806`](https://github.com/gradido/gradido/pull/1806)
+- devops: Deploy Seed in Backend [`#1790`](https://github.com/gradido/gradido/pull/1790)
+- refactor: Balance Model and Decay Rounding [`#1780`](https://github.com/gradido/gradido/pull/1780)
+- change config DECAY_START_TIME in UTC 0000 [`#1807`](https://github.com/gradido/gradido/pull/1807)
+- 1751 make gdt visible only if explicitly clicked [`#1752`](https://github.com/gradido/gradido/pull/1752)
+- add Tab system from bootstrap in SearchUserTable Userdata [`#1744`](https://github.com/gradido/gradido/pull/1744)
+- Fix: Certbot renewal [`#1789`](https://github.com/gradido/gradido/pull/1789)
+- 🍰 Add Wallet Link To Mails [`#1765`](https://github.com/gradido/gradido/pull/1765)
+- 1633 display qr code on link in transaction list [`#1661`](https://github.com/gradido/gradido/pull/1661)
+- 1755 insert additional text when redeeming [`#1756`](https://github.com/gradido/gradido/pull/1756)
+- refactor: Define Context Interface [`#1762`](https://github.com/gradido/gradido/pull/1762)
+- fix: Elopage Status [`#1742`](https://github.com/gradido/gradido/pull/1742)
+- Refactor: Frontend decay start block as static config value [`#1749`](https://github.com/gradido/gradido/pull/1749)
+- better date format for reddem valid date [`#1758`](https://github.com/gradido/gradido/pull/1758)
+- add insert shadow in summary links transaction type [`#1754`](https://github.com/gradido/gradido/pull/1754)
+- Feature: JWT duration is now 30min by default [`#1747`](https://github.com/gradido/gradido/pull/1747)
+- Docu: Scope of Gradido [`#1746`](https://github.com/gradido/gradido/pull/1746)
+- fix: Check That Recipient User Has Activated Account to Receive Coins [`#1743`](https://github.com/gradido/gradido/pull/1743)
+- Fix: Fixed config dist version to properly reflect new password reset url [`#1737`](https://github.com/gradido/gradido/pull/1737)
+- 503 transaction list pagination pages clickable [`#1677`](https://github.com/gradido/gradido/pull/1677)
+- if no recipientEmail else form.email [`#1722`](https://github.com/gradido/gradido/pull/1722)
+- 1727 change button text and observe spelling [`#1728`](https://github.com/gradido/gradido/pull/1728)
+- 1729 load spinner if pending balance [`#1730`](https://github.com/gradido/gradido/pull/1730)
+- transaction type remains when jumping from the verification back [`#1724`](https://github.com/gradido/gradido/pull/1724)
+- text for toast expand link copied [`#1726`](https://github.com/gradido/gradido/pull/1726)
+
#### [1.7.1](https://github.com/gradido/gradido/compare/1.7.0...1.7.1)
+> 1 April 2022
+
+- v1.7.1 [`#1721`](https://github.com/gradido/gradido/pull/1721)
- fix: Localize Dates on Redeem Transaction Link Page [`#1720`](https://github.com/gradido/gradido/pull/1720)
- fix: Round Virtual Transaction Link Transaction [`#1718`](https://github.com/gradido/gradido/pull/1718)
- larger icon and deacy information if center [`#1719`](https://github.com/gradido/gradido/pull/1719)
diff --git a/admin/package.json b/admin/package.json
index 3d3919954..1ff6de770 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
- "version": "1.7.1",
+ "version": "1.8.1",
"license": "MIT",
"private": false,
"scripts": {
diff --git a/admin/src/components/CreationFormular.spec.js b/admin/src/components/CreationFormular.spec.js
index 083b7ca67..08ec71bdc 100644
--- a/admin/src/components/CreationFormular.spec.js
+++ b/admin/src/components/CreationFormular.spec.js
@@ -24,12 +24,6 @@ const mocks = {
},
$store: {
commit: stateCommitMock,
- state: {
- moderator: {
- id: 0,
- name: 'test moderator',
- },
- },
},
}
@@ -122,7 +116,6 @@ describe('CreationFormular', () => {
creationDate: getCreationDate(2),
amount: 90,
memo: 'Test create coins',
- moderator: 0,
},
}),
)
@@ -370,14 +363,12 @@ describe('CreationFormular', () => {
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
- moderator: 0,
},
{
email: 'bibi@bloxberg.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
- moderator: 0,
},
],
},
diff --git a/admin/src/components/CreationFormular.vue b/admin/src/components/CreationFormular.vue
index cd4de5fd6..cdcd6ef1d 100644
--- a/admin/src/components/CreationFormular.vue
+++ b/admin/src/components/CreationFormular.vue
@@ -154,7 +154,6 @@ export default {
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
- moderator: Number(this.$store.state.moderator.id),
})
})
this.$apollo
@@ -188,7 +187,6 @@ export default {
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
- moderator: Number(this.$store.state.moderator.id),
}
this.$apollo
.mutate({
diff --git a/admin/src/components/EditCreationFormular.spec.js b/admin/src/components/EditCreationFormular.spec.js
index f5c7fb0fe..f39edad52 100644
--- a/admin/src/components/EditCreationFormular.spec.js
+++ b/admin/src/components/EditCreationFormular.spec.js
@@ -11,7 +11,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
amount: 500,
date: new Date(),
memo: 'Test Schöpfung 2',
- moderator: 0,
},
},
})
@@ -28,12 +27,6 @@ const mocks = {
mutate: apolloMutateMock,
},
$store: {
- state: {
- moderator: {
- id: 0,
- name: 'test moderator',
- },
- },
commit: stateCommitMock,
},
}
@@ -104,7 +97,6 @@ describe('EditCreationFormular', () => {
creationDate: getCreationDate(0),
amount: 500,
memo: 'Test Schöpfung 2',
- moderator: 0,
},
}),
)
@@ -129,7 +121,6 @@ describe('EditCreationFormular', () => {
amount: 500,
date: expect.any(Date),
memo: 'Test Schöpfung 2',
- moderator: 0,
row: expect.any(Object),
},
],
diff --git a/admin/src/components/EditCreationFormular.vue b/admin/src/components/EditCreationFormular.vue
index 82b444154..fb30f2b77 100644
--- a/admin/src/components/EditCreationFormular.vue
+++ b/admin/src/components/EditCreationFormular.vue
@@ -120,7 +120,6 @@ export default {
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
- moderator: Number(this.$store.state.moderator.id),
},
})
.then((result) => {
@@ -129,7 +128,6 @@ export default {
amount: Number(result.data.updatePendingCreation.amount),
date: result.data.updatePendingCreation.date,
memo: result.data.updatePendingCreation.memo,
- moderator: Number(result.data.updatePendingCreation.moderator),
row: this.row,
})
this.toastSuccess(
diff --git a/admin/src/graphql/createPendingCreation.js b/admin/src/graphql/createPendingCreation.js
index 183fa5b15..9301ea489 100644
--- a/admin/src/graphql/createPendingCreation.js
+++ b/admin/src/graphql/createPendingCreation.js
@@ -1,19 +1,7 @@
import gql from 'graphql-tag'
export const createPendingCreation = gql`
- mutation (
- $email: String!
- $amount: Float!
- $memo: String!
- $creationDate: String!
- $moderator: Int!
- ) {
- createPendingCreation(
- email: $email
- amount: $amount
- memo: $memo
- creationDate: $creationDate
- moderator: $moderator
- )
+ mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
+ createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
}
`
diff --git a/admin/src/graphql/searchUsers.js b/admin/src/graphql/searchUsers.js
index e28508d1b..ddf759031 100644
--- a/admin/src/graphql/searchUsers.js
+++ b/admin/src/graphql/searchUsers.js
@@ -5,15 +5,15 @@ export const searchUsers = gql`
$searchText: String!
$currentPage: Int
$pageSize: Int
- $notActivated: Boolean
- $isDeleted: Boolean
+ $filterByActivated: Boolean
+ $filterByDeleted: Boolean
) {
searchUsers(
searchText: $searchText
currentPage: $currentPage
pageSize: $pageSize
- notActivated: $notActivated
- isDeleted: $isDeleted
+ filterByActivated: $filterByActivated
+ filterByDeleted: $filterByDeleted
) {
userCount
userList {
diff --git a/admin/src/graphql/updatePendingCreation.js b/admin/src/graphql/updatePendingCreation.js
index 77668f15b..f0775e68b 100644
--- a/admin/src/graphql/updatePendingCreation.js
+++ b/admin/src/graphql/updatePendingCreation.js
@@ -1,27 +1,18 @@
import gql from 'graphql-tag'
export const updatePendingCreation = gql`
- mutation (
- $id: Int!
- $email: String!
- $amount: Float!
- $memo: String!
- $creationDate: String!
- $moderator: Int!
- ) {
+ mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
updatePendingCreation(
id: $id
email: $email
amount: $amount
memo: $memo
creationDate: $creationDate
- moderator: $moderator
) {
amount
date
memo
creation
- moderator
}
}
`
diff --git a/admin/src/pages/Creation.spec.js b/admin/src/pages/Creation.spec.js
index 204c35817..98c03d277 100644
--- a/admin/src/pages/Creation.spec.js
+++ b/admin/src/pages/Creation.spec.js
@@ -71,8 +71,8 @@ describe('Creation', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filterByActivated: true,
+ filterByDeleted: false,
},
}),
)
@@ -271,8 +271,8 @@ describe('Creation', () => {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filterByActivated: true,
+ filterByDeleted: false,
},
}),
)
@@ -288,8 +288,8 @@ describe('Creation', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filterByActivated: true,
+ filterByDeleted: false,
},
}),
)
@@ -305,8 +305,8 @@ describe('Creation', () => {
searchText: '',
currentPage: 2,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filterByActivated: true,
+ filterByDeleted: false,
},
}),
)
diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue
index e5b93350f..54bc0d735 100644
--- a/admin/src/pages/Creation.vue
+++ b/admin/src/pages/Creation.vue
@@ -102,8 +102,8 @@ export default {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
- notActivated: false,
- isDeleted: false,
+ filterByActivated: true,
+ filterByDeleted: false,
},
fetchPolicy: 'network-only',
})
diff --git a/admin/src/pages/UserSearch.spec.js b/admin/src/pages/UserSearch.spec.js
index 0b98d4d11..2eb24f84b 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -82,8 +82,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filterByActivated: null,
+ filterByDeleted: null,
},
}),
)
@@ -101,8 +101,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: true,
- isDeleted: null,
+ filterByActivated: false,
+ filterByDeleted: null,
},
}),
)
@@ -121,8 +121,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: true,
+ filterByActivated: null,
+ filterByDeleted: true,
},
}),
)
@@ -141,8 +141,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 2,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filterByActivated: null,
+ filterByDeleted: null,
},
}),
)
@@ -161,8 +161,8 @@ describe('UserSearch', () => {
searchText: 'search string',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filterByActivated: null,
+ filterByDeleted: null,
},
}),
)
@@ -178,8 +178,8 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filterByActivated: null,
+ filterByDeleted: null,
},
}),
)
diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue
index b2737bae6..f8ceac36c 100644
--- a/admin/src/pages/UserSearch.vue
+++ b/admin/src/pages/UserSearch.vue
@@ -3,11 +3,23 @@
- {{ filterCheckedEmails ? $t('unregistered_emails') : $t('all_emails') }}
+ {{
+ filterByActivated === null
+ ? $t('all_emails')
+ : filterByActivated === false
+ ? $t('unregistered_emails')
+ : ''
+ }}
- {{ filterDeletedUser ? $t('deleted_user') : $t('all_emails') }}
+ {{
+ filterByDeleted === null
+ ? $t('all_emails')
+ : filterByDeleted === true
+ ? $t('deleted_user')
+ : ''
+ }}
@@ -60,8 +72,8 @@ export default {
searchResult: [],
massCreation: [],
criteria: '',
- filterCheckedEmails: null,
- filterDeletedUser: null,
+ filterByActivated: null,
+ filterByDeleted: null,
rows: 0,
currentPage: 1,
perPage: 25,
@@ -70,11 +82,11 @@ export default {
},
methods: {
unconfirmedRegisterMails() {
- this.filterCheckedEmails = this.filterCheckedEmails ? null : true
+ this.filterByActivated = this.filterByActivated === null ? false : null
this.getUsers()
},
deletedUserSearch() {
- this.filterDeletedUser = this.filterDeletedUser ? null : true
+ this.filterByDeleted = this.filterByDeleted === null ? true : null
this.getUsers()
},
getUsers() {
@@ -85,8 +97,8 @@ export default {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
- notActivated: this.filterCheckedEmails,
- isDeleted: this.filterDeletedUser,
+ filterByActivated: this.filterByActivated,
+ filterByDeleted: this.filterByDeleted,
},
fetchPolicy: 'no-cache',
})
diff --git a/backend/package.json b/backend/package.json
index 8654f4cc7..f195ab44f 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "gradido-backend",
- "version": "1.7.1",
+ "version": "1.8.1",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@@ -14,7 +14,7 @@
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
- "seed": "cross-env TZ=UTC ts-node -r tsconfig-paths/register src/seeds/index.ts"
+ "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts"
},
"dependencies": {
"@types/jest": "^27.0.2",
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index 86cefe9e6..1eee1b9a4 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
- DB_VERSION: '0033-add_referrer_id',
+ DB_VERSION: '0035-admin_pending_creations_decimal',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
diff --git a/backend/src/graphql/arg/CreatePendingCreationArgs.ts b/backend/src/graphql/arg/CreatePendingCreationArgs.ts
index b90ad3231..11c345465 100644
--- a/backend/src/graphql/arg/CreatePendingCreationArgs.ts
+++ b/backend/src/graphql/arg/CreatePendingCreationArgs.ts
@@ -1,4 +1,5 @@
-import { ArgsType, Field, Float, InputType, Int } from 'type-graphql'
+import { ArgsType, Field, InputType } from 'type-graphql'
+import Decimal from 'decimal.js-light'
@InputType()
@ArgsType()
@@ -6,15 +7,12 @@ export default class CreatePendingCreationArgs {
@Field(() => String)
email: string
- @Field(() => Float)
- amount: number
+ @Field(() => Decimal)
+ amount: Decimal
@Field(() => String)
memo: string
@Field(() => String)
creationDate: string
-
- @Field(() => Int)
- moderator: number
}
diff --git a/backend/src/graphql/arg/SearchUsersArgs.ts b/backend/src/graphql/arg/SearchUsersArgs.ts
index 2a94d8998..b47f39d56 100644
--- a/backend/src/graphql/arg/SearchUsersArgs.ts
+++ b/backend/src/graphql/arg/SearchUsersArgs.ts
@@ -12,8 +12,8 @@ export default class SearchUsersArgs {
pageSize?: number
@Field(() => Boolean, { nullable: true })
- notActivated?: boolean | null
+ filterByActivated?: boolean | null
@Field(() => Boolean, { nullable: true })
- isDeleted?: boolean | null
+ filterByDeleted?: boolean | null
}
diff --git a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts
index 73f70c058..691d73154 100644
--- a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts
+++ b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts
@@ -1,4 +1,5 @@
-import { ArgsType, Field, Float, Int } from 'type-graphql'
+import { ArgsType, Field, Int } from 'type-graphql'
+import Decimal from 'decimal.js-light'
@ArgsType()
export default class UpdatePendingCreationArgs {
@@ -8,15 +9,12 @@ export default class UpdatePendingCreationArgs {
@Field(() => String)
email: string
- @Field(() => Float)
- amount: number
+ @Field(() => Decimal)
+ amount: Decimal
@Field(() => String)
memo: string
@Field(() => String)
creationDate: string
-
- @Field(() => Int)
- moderator: number
}
diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts
index 84756c45a..065c01957 100644
--- a/backend/src/graphql/directive/isAuthorized.ts
+++ b/backend/src/graphql/directive/isAuthorized.ts
@@ -8,7 +8,6 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { getCustomRepository } from '@dbTools/typeorm'
import { UserRepository } from '@repository/User'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
-import { ServerUser } from '@entity/ServerUser'
const isAuthorized: AuthChecker = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user
@@ -36,8 +35,7 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => {
try {
const user = await userRepository.findByPubkeyHex(context.pubKey)
context.user = user
- const countServerUsers = await ServerUser.count({ email: user.email })
- context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
+ context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
} catch {
// in case the database query fails (user deleted)
throw new Error('401 Unauthorized')
diff --git a/backend/src/graphql/model/PendingCreation.ts b/backend/src/graphql/model/PendingCreation.ts
index 594657a59..500ba6f6b 100644
--- a/backend/src/graphql/model/PendingCreation.ts
+++ b/backend/src/graphql/model/PendingCreation.ts
@@ -1,4 +1,5 @@
import { ObjectType, Field, Int } from 'type-graphql'
+import Decimal from 'decimal.js-light'
@ObjectType()
export class PendingCreation {
@@ -23,12 +24,12 @@ export class PendingCreation {
@Field(() => String)
memo: string
- @Field(() => Number)
- amount: number
+ @Field(() => Decimal)
+ amount: Decimal
@Field(() => Number)
moderator: number
- @Field(() => [Number])
- creation: number[]
+ @Field(() => [Decimal])
+ creation: Decimal[]
}
diff --git a/backend/src/graphql/model/UpdatePendingCreation.ts b/backend/src/graphql/model/UpdatePendingCreation.ts
index c8033f86e..e19e1e064 100644
--- a/backend/src/graphql/model/UpdatePendingCreation.ts
+++ b/backend/src/graphql/model/UpdatePendingCreation.ts
@@ -1,4 +1,5 @@
import { ObjectType, Field } from 'type-graphql'
+import Decimal from 'decimal.js-light'
@ObjectType()
export class UpdatePendingCreation {
@@ -8,12 +9,9 @@ export class UpdatePendingCreation {
@Field(() => String)
memo: string
- @Field(() => Number)
- amount: number
+ @Field(() => Decimal)
+ amount: Decimal
- @Field(() => Number)
- moderator: number
-
- @Field(() => [Number])
- creation: number[]
+ @Field(() => [Decimal])
+ creation: Decimal[]
}
diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts
index 1a187a38f..4f577f60a 100644
--- a/backend/src/graphql/model/User.ts
+++ b/backend/src/graphql/model/User.ts
@@ -14,8 +14,8 @@ export class User {
this.emailChecked = user.emailChecked
this.language = user.language
this.publisherId = user.publisherId
+ this.isAdmin = user.isAdmin
// TODO
- this.isAdmin = null
this.coinanimation = null
this.klickTipp = null
this.hasElopage = null
@@ -58,11 +58,11 @@ export class User {
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ @Field(() => Date, { nullable: true })
+ isAdmin: Date | null
+
// TODO this is a bit inconsistent with what we query from the database
// therefore all those fields are now nullable with default value null
- @Field(() => Boolean, { nullable: true })
- isAdmin: boolean | null
-
@Field(() => Boolean, { nullable: true })
coinanimation: boolean | null
diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts
index 1d418c66c..8a1459c0f 100644
--- a/backend/src/graphql/model/UserAdmin.ts
+++ b/backend/src/graphql/model/UserAdmin.ts
@@ -1,9 +1,10 @@
-import { User } from '@entity/User'
import { ObjectType, Field, Int } from 'type-graphql'
+import Decimal from 'decimal.js-light'
+import { User } from '@entity/User'
@ObjectType()
export class UserAdmin {
- constructor(user: User, creation: number[], hasElopage: boolean, emailConfirmationSend: string) {
+ constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
this.userId = user.id
this.email = user.email
this.firstName = user.firstName
@@ -27,8 +28,8 @@ export class UserAdmin {
@Field(() => String)
lastName: string
- @Field(() => [Number])
- creation: number[]
+ @Field(() => [Decimal])
+ creation: Decimal[]
@Field(() => Boolean)
emailChecked: boolean
diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts
new file mode 100644
index 000000000..6842f09ca
--- /dev/null
+++ b/backend/src/graphql/resolver/AdminResolver.test.ts
@@ -0,0 +1,1060 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+
+import { testEnvironment, resetToken, cleanDB } from '@test/helpers'
+import { userFactory } from '@/seeds/factory/user'
+import { creationFactory } from '@/seeds/factory/creation'
+import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
+import { peterLustig } from '@/seeds/users/peter-lustig'
+import { stephenHawking } from '@/seeds/users/stephen-hawking'
+import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
+import {
+ deleteUser,
+ unDeleteUser,
+ createPendingCreation,
+ createPendingCreations,
+ updatePendingCreation,
+ deletePendingCreation,
+ confirmPendingCreation,
+} from '@/seeds/graphql/mutations'
+import { getPendingCreations, login } from '@/seeds/graphql/queries'
+import { GraphQLError } from 'graphql'
+import { User } from '@entity/User'
+/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
+import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
+import Decimal from 'decimal.js-light'
+import { AdminPendingCreation } from '@entity/AdminPendingCreation'
+import { Transaction as DbTransaction } from '@entity/Transaction'
+
+// mock account activation email to avoid console spam
+jest.mock('@/mailer/sendAccountActivationEmail', () => {
+ return {
+ __esModule: true,
+ sendAccountActivationEmail: jest.fn(),
+ }
+})
+
+let mutate: any, query: any, con: any
+let testEnv: any
+
+beforeAll(async () => {
+ testEnv = await testEnvironment()
+ mutate = testEnv.mutate
+ query = testEnv.query
+ con = testEnv.con
+ await cleanDB()
+})
+
+afterAll(async () => {
+ await cleanDB()
+ await con.close()
+})
+
+let admin: User
+let user: User
+let creation: AdminPendingCreation | void
+
+describe('AdminResolver', () => {
+ describe('delete user', () => {
+ describe('unauthenticated', () => {
+ it('returns an error', async () => {
+ await expect(mutate({ mutation: deleteUser, variables: { userId: 1 } })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ describe('without admin rights', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ await query({
+ query: login,
+ variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ it('returns an error', async () => {
+ await expect(
+ mutate({ mutation: deleteUser, variables: { userId: user.id + 1 } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('with admin rights', () => {
+ beforeAll(async () => {
+ admin = await userFactory(testEnv, peterLustig)
+ await query({
+ query: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ describe('user to be deleted does not exist', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
+ }),
+ )
+ })
+ })
+
+ describe('delete self', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: deleteUser, variables: { userId: admin.id } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Moderator can not delete his own account!')],
+ }),
+ )
+ })
+ })
+
+ describe('delete with success', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ })
+
+ it('returns date string', async () => {
+ const result = await mutate({ mutation: deleteUser, variables: { userId: user.id } })
+ expect(result).toEqual(
+ expect.objectContaining({
+ data: {
+ deleteUser: expect.any(String),
+ },
+ }),
+ )
+ expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date))
+ })
+
+ describe('delete deleted user', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: deleteUser, variables: { userId: user.id } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError(`Could not find user with userId: ${user.id}`)],
+ }),
+ )
+ })
+ })
+ })
+ })
+ })
+ })
+
+ describe('unDelete user', () => {
+ describe('unauthenticated', () => {
+ it('returns an error', async () => {
+ await expect(mutate({ mutation: unDeleteUser, variables: { userId: 1 } })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ describe('without admin rights', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ await query({
+ query: login,
+ variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ it('returns an error', async () => {
+ await expect(
+ mutate({ mutation: unDeleteUser, variables: { userId: user.id + 1 } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('with admin rights', () => {
+ beforeAll(async () => {
+ admin = await userFactory(testEnv, peterLustig)
+ await query({
+ query: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ describe('user to be undelete does not exist', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
+ }),
+ )
+ })
+ })
+
+ describe('user to undelete is not deleted', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ })
+
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: unDeleteUser, variables: { userId: user.id } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('User is not deleted')],
+ }),
+ )
+ })
+
+ describe('undelete deleted user', () => {
+ beforeAll(async () => {
+ await mutate({ mutation: deleteUser, variables: { userId: user.id } })
+ })
+
+ it('returns null', async () => {
+ await expect(
+ mutate({ mutation: unDeleteUser, variables: { userId: user.id } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: { unDeleteUser: null },
+ }),
+ )
+ })
+ })
+ })
+ })
+ })
+ })
+
+ describe('creations', () => {
+ const variables = {
+ email: 'bibi@bloxberg.de',
+ amount: new Decimal(2000),
+ memo: 'Aktives Grundeinkommen',
+ creationDate: 'not-valid',
+ }
+
+ describe('unauthenticated', () => {
+ describe('createPendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('createPendingCreations', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: createPendingCreations,
+ variables: { pendingCreations: [variables] },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('updatePendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: 1,
+ email: 'bibi@bloxberg.de',
+ amount: new Decimal(300),
+ memo: 'Danke Bibi!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('getPendingCreations', () => {
+ it('returns an error', async () => {
+ await expect(
+ query({
+ query: getPendingCreations,
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('deletePendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: deletePendingCreation,
+ variables: {
+ id: 1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('confirmPendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: 1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+ })
+
+ describe('authenticated', () => {
+ describe('without admin rights', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ await query({
+ query: login,
+ variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ describe('createPendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('createPendingCreations', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: createPendingCreations,
+ variables: { pendingCreations: [variables] },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('updatePendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: 1,
+ email: 'bibi@bloxberg.de',
+ amount: new Decimal(300),
+ memo: 'Danke Bibi!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('getPendingCreations', () => {
+ it('returns an error', async () => {
+ await expect(
+ query({
+ query: getPendingCreations,
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('deletePendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: deletePendingCreation,
+ variables: {
+ id: 1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('confirmPendingCreation', () => {
+ it('returns an error', async () => {
+ await expect(
+ mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: 1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+ })
+
+ describe('with admin rights', () => {
+ beforeAll(async () => {
+ admin = await userFactory(testEnv, peterLustig)
+ await query({
+ query: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ describe('createPendingCreation', () => {
+ beforeAll(async () => {
+ const now = new Date()
+ creation = await creationFactory(testEnv, {
+ email: 'peter@lustig.de',
+ amount: 400,
+ memo: 'Herzlich Willkommen bei Gradido!',
+ creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(),
+ })
+ })
+
+ describe('user to create for does not exist', () => {
+ it('throws an error', async () => {
+ await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')],
+ }),
+ )
+ })
+ })
+
+ describe('user to create for is deleted', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, stephenHawking)
+ variables.email = 'stephen@hawking.uk'
+ })
+
+ it('throws an error', async () => {
+ await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('This user was deleted. Cannot make a creation.')],
+ }),
+ )
+ })
+ })
+
+ describe('user to create for has email not confirmed', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, garrickOllivander)
+ variables.email = 'garrick@ollivander.com'
+ })
+
+ it('throws an error', async () => {
+ await expect(mutate({ mutation: createPendingCreation, variables })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Creation could not be saved, Email is not activated')],
+ }),
+ )
+ })
+ })
+
+ describe('valid user to create for', () => {
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ variables.email = 'bibi@bloxberg.de'
+ })
+
+ describe('date of creation is not a date string', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({ mutation: createPendingCreation, variables }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError('No information for available creations for the given date'),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('date of creation is four months ago', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ variables.creationDate = new Date(
+ now.getFullYear(),
+ now.getMonth() - 4,
+ 1,
+ ).toString()
+ await expect(
+ mutate({ mutation: createPendingCreation, variables }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError('No information for available creations for the given date'),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('date of creation is in the future', () => {
+ it('throws an error', async () => {
+ const now = new Date()
+ variables.creationDate = new Date(
+ now.getFullYear(),
+ now.getMonth() + 4,
+ 1,
+ ).toString()
+ await expect(
+ mutate({ mutation: createPendingCreation, variables }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError('No information for available creations for the given date'),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('amount of creation is too high', () => {
+ it('throws an error', async () => {
+ variables.creationDate = new Date().toString()
+ await expect(
+ mutate({ mutation: createPendingCreation, variables }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError(
+ 'The amount (2000 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
+ ),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('creation is valid', () => {
+ it('returns an array of the open creations for the last three months', async () => {
+ variables.amount = new Decimal(200)
+ await expect(
+ mutate({ mutation: createPendingCreation, variables }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ createPendingCreation: [1000, 1000, 800],
+ },
+ }),
+ )
+ })
+ })
+
+ describe('second creation surpasses the available amount ', () => {
+ it('returns an array of the open creations for the last three months', async () => {
+ variables.amount = new Decimal(1000)
+ await expect(
+ mutate({ mutation: createPendingCreation, variables }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError(
+ 'The amount (1000 GDD) to be created exceeds the amount (800 GDD) still available for this month.',
+ ),
+ ],
+ }),
+ )
+ })
+ })
+ })
+ })
+
+ describe('createPendingCreations', () => {
+ // at this point we have this data in DB:
+ // bibi@bloxberg.de: [1000, 1000, 800]
+ // peter@lustig.de: [1000, 600, 1000]
+ // stephen@hawking.uk: [1000, 1000, 1000] - deleted
+ // garrick@ollivander.com: [1000, 1000, 1000] - not activated
+
+ const massCreationVariables = [
+ 'bibi@bloxberg.de',
+ 'peter@lustig.de',
+ 'stephen@hawking.uk',
+ 'garrick@ollivander.com',
+ 'bob@baumeister.de',
+ ].map((email) => {
+ return {
+ email,
+ amount: new Decimal(500),
+ memo: 'Grundeinkommen',
+ creationDate: new Date().toString(),
+ }
+ })
+
+ it('returns success, two successful creation and three failed creations', async () => {
+ await expect(
+ mutate({
+ mutation: createPendingCreations,
+ variables: { pendingCreations: massCreationVariables },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ createPendingCreations: {
+ success: true,
+ successfulCreation: ['bibi@bloxberg.de', 'peter@lustig.de'],
+ failedCreation: [
+ 'stephen@hawking.uk',
+ 'garrick@ollivander.com',
+ 'bob@baumeister.de',
+ ],
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ describe('updatePendingCreation', () => {
+ // at this I expect to have this data in DB:
+ // bibi@bloxberg.de: [1000, 1000, 300]
+ // peter@lustig.de: [1000, 600, 500]
+ // stephen@hawking.uk: [1000, 1000, 1000] - deleted
+ // garrick@ollivander.com: [1000, 1000, 1000] - not activated
+
+ describe('user for creation to update does not exist', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: 1,
+ email: 'bob@baumeister.de',
+ amount: new Decimal(300),
+ memo: 'Danke Bibi!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Could not find user with email: bob@baumeister.de')],
+ }),
+ )
+ })
+ })
+
+ describe('user for creation to update is deleted', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: 1,
+ email: 'stephen@hawking.uk',
+ amount: new Decimal(300),
+ memo: 'Danke Bibi!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('User was deleted (stephen@hawking.uk)')],
+ }),
+ )
+ })
+ })
+
+ describe('creation does not exist', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: -1,
+ email: 'bibi@bloxberg.de',
+ amount: new Decimal(300),
+ memo: 'Danke Bibi!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('No creation found to given id.')],
+ }),
+ )
+ })
+ })
+
+ describe('user email does not match creation user', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ email: 'bibi@bloxberg.de',
+ amount: new Decimal(300),
+ memo: 'Danke Bibi!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError(
+ 'user of the pending creation and send user does not correspond',
+ ),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('creation update is not valid', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ email: 'peter@lustig.de',
+ amount: new Decimal(1900),
+ memo: 'Danke Peter!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError(
+ 'The amount (1900 GDD) to be created exceeds the amount (500 GDD) still available for this month.',
+ ),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('creation update is successful changing month', () => {
+ it('returns update creation object', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ email: 'peter@lustig.de',
+ amount: new Decimal(300),
+ memo: 'Danke Peter!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ updatePendingCreation: {
+ date: expect.any(String),
+ memo: 'Danke Peter!',
+ amount: '300',
+ creation: ['1000', '1000', '200'],
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ describe('creation update is successful without changing month', () => {
+ it('returns update creation object', async () => {
+ await expect(
+ mutate({
+ mutation: updatePendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ email: 'peter@lustig.de',
+ amount: new Decimal(200),
+ memo: 'Das war leider zu Viel!',
+ creationDate: new Date().toString(),
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ updatePendingCreation: {
+ date: expect.any(String),
+ memo: 'Das war leider zu Viel!',
+ amount: '200',
+ creation: ['1000', '1000', '300'],
+ },
+ },
+ }),
+ )
+ })
+ })
+ })
+
+ describe('getPendingCreations', () => {
+ it('returns four pending creations', async () => {
+ await expect(
+ query({
+ query: getPendingCreations,
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ getPendingCreations: expect.arrayContaining([
+ {
+ id: expect.any(Number),
+ firstName: 'Peter',
+ lastName: 'Lustig',
+ email: 'peter@lustig.de',
+ date: expect.any(String),
+ memo: 'Das war leider zu Viel!',
+ amount: '200',
+ moderator: admin.id,
+ creation: ['1000', '1000', '300'],
+ },
+ {
+ id: expect.any(Number),
+ firstName: 'Peter',
+ lastName: 'Lustig',
+ email: 'peter@lustig.de',
+ date: expect.any(String),
+ memo: 'Grundeinkommen',
+ amount: '500',
+ moderator: admin.id,
+ creation: ['1000', '1000', '300'],
+ },
+ {
+ id: expect.any(Number),
+ firstName: 'Bibi',
+ lastName: 'Bloxberg',
+ email: 'bibi@bloxberg.de',
+ date: expect.any(String),
+ memo: 'Grundeinkommen',
+ amount: '500',
+ moderator: admin.id,
+ creation: ['1000', '1000', '300'],
+ },
+ {
+ id: expect.any(Number),
+ firstName: 'Bibi',
+ lastName: 'Bloxberg',
+ email: 'bibi@bloxberg.de',
+ date: expect.any(String),
+ memo: 'Aktives Grundeinkommen',
+ amount: '200',
+ moderator: admin.id,
+ creation: ['1000', '1000', '300'],
+ },
+ ]),
+ },
+ }),
+ )
+ })
+ })
+
+ describe('deletePendingCreation', () => {
+ describe('creation id does not exist', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: deletePendingCreation,
+ variables: {
+ id: -1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Creation not found for given id.')],
+ }),
+ )
+ })
+ })
+
+ describe('creation id does exist', () => {
+ it('returns true', async () => {
+ await expect(
+ mutate({
+ mutation: deletePendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: { deletePendingCreation: true },
+ }),
+ )
+ })
+ })
+ })
+
+ describe('confirmPendingCreation', () => {
+ describe('creation does not exits', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: -1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Creation not found to given id.')],
+ }),
+ )
+ })
+ })
+
+ describe('confirm own creation', () => {
+ beforeAll(async () => {
+ const now = new Date()
+ creation = await creationFactory(testEnv, {
+ email: 'peter@lustig.de',
+ amount: 400,
+ memo: 'Herzlich Willkommen bei Gradido!',
+ creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(),
+ })
+ })
+
+ it('thows an error', async () => {
+ await expect(
+ mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Moderator can not confirm own pending creation')],
+ }),
+ )
+ })
+ })
+
+ describe('confirm creation for other user', () => {
+ beforeAll(async () => {
+ const now = new Date()
+ creation = await creationFactory(testEnv, {
+ email: 'bibi@bloxberg.de',
+ amount: 450,
+ memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
+ creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
+ })
+ })
+
+ it('returns true', async () => {
+ await expect(
+ mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: creation ? creation.id : -1,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: { confirmPendingCreation: true },
+ }),
+ )
+ })
+
+ it('creates a transaction', async () => {
+ const transaction = await DbTransaction.find()
+ expect(transaction[0].amount.toString()).toBe('450')
+ expect(transaction[0].memo).toBe('Herzlich Willkommen bei Gradido liebe Bibi!')
+ expect(transaction[0].linkedTransactionId).toEqual(null)
+ expect(transaction[0].transactionLinkId).toEqual(null)
+ expect(transaction[0].previous).toEqual(null)
+ expect(transaction[0].linkedUserId).toEqual(null)
+ expect(transaction[0].typeId).toEqual(1)
+ })
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index 7ca3460ee..78cbf3fc8 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -43,7 +43,7 @@ import CONFIG from '@/config'
// const EMAIL_OPT_IN_REGISTER = 1
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
-const MAX_CREATION_AMOUNT = 1000
+const MAX_CREATION_AMOUNT = new Decimal(1000)
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
@Resolver()
@@ -56,19 +56,19 @@ export class AdminResolver {
searchText,
currentPage = 1,
pageSize = 25,
- notActivated = null,
- isDeleted = null,
+ filterByActivated = null,
+ filterByDeleted = null,
}: SearchUsersArgs,
): Promise {
const userRepository = getCustomRepository(UserRepository)
const filterCriteria: ObjectLiteral[] = []
- if (notActivated !== null) {
- filterCriteria.push({ emailChecked: !notActivated })
+ if (filterByActivated !== null) {
+ filterCriteria.push({ emailChecked: filterByActivated })
}
- if (isDeleted !== null) {
- filterCriteria.push({ deletedAt: isDeleted ? Not(IsNull()) : IsNull() })
+ if (filterByDeleted !== null) {
+ filterCriteria.push({ deletedAt: filterByDeleted ? Not(IsNull()) : IsNull() })
}
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
@@ -157,11 +157,12 @@ export class AdminResolver {
@Mutation(() => Date, { nullable: true })
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise {
const user = await dbUser.findOne({ id: userId }, { withDeleted: true })
- // user exists ?
if (!user) {
throw new Error(`Could not find user with userId: ${userId}`)
}
- // recover user account
+ if (!user.deletedAt) {
+ throw new Error('User is not deleted')
+ }
await user.recover()
return null
}
@@ -169,8 +170,9 @@ export class AdminResolver {
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
@Mutation(() => [Number])
async createPendingCreation(
- @Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
- ): Promise {
+ @Args() { email, amount, memo, creationDate }: CreatePendingCreationArgs,
+ @Ctx() context: Context,
+ ): Promise {
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
throw new Error(`Could not find user with email: ${email}`)
@@ -181,16 +183,17 @@ export class AdminResolver {
if (!user.emailChecked) {
throw new Error('Creation could not be saved, Email is not activated')
}
+ const moderator = getUser(context)
const creations = await getUserCreation(user.id)
const creationDateObj = new Date(creationDate)
if (isCreationValid(creations, amount, creationDateObj)) {
const adminPendingCreation = AdminPendingCreation.create()
adminPendingCreation.userId = user.id
- adminPendingCreation.amount = BigInt(amount)
+ adminPendingCreation.amount = amount
adminPendingCreation.created = new Date()
adminPendingCreation.date = creationDateObj
adminPendingCreation.memo = memo
- adminPendingCreation.moderator = moderator
+ adminPendingCreation.moderator = moderator.id
await AdminPendingCreation.save(adminPendingCreation)
}
@@ -202,12 +205,13 @@ export class AdminResolver {
async createPendingCreations(
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
pendingCreations: CreatePendingCreationArgs[],
+ @Ctx() context: Context,
): Promise {
let success = false
const successfulCreation: string[] = []
const failedCreation: string[] = []
for (const pendingCreation of pendingCreations) {
- await this.createPendingCreation(pendingCreation)
+ await this.createPendingCreation(pendingCreation, context)
.then(() => {
successfulCreation.push(pendingCreation.email)
success = true
@@ -226,7 +230,8 @@ export class AdminResolver {
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
@Mutation(() => UpdatePendingCreation)
async updatePendingCreation(
- @Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs,
+ @Args() { id, email, amount, memo, creationDate }: UpdatePendingCreationArgs,
+ @Ctx() context: Context,
): Promise {
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
@@ -236,7 +241,13 @@ export class AdminResolver {
throw new Error(`User was deleted (${email})`)
}
- const pendingCreationToUpdate = await AdminPendingCreation.findOneOrFail({ id })
+ const moderator = getUser(context)
+
+ const pendingCreationToUpdate = await AdminPendingCreation.findOne({ id })
+
+ if (!pendingCreationToUpdate) {
+ throw new Error('No creation found to given id.')
+ }
if (pendingCreationToUpdate.userId !== user.id) {
throw new Error('user of the pending creation and send user does not correspond')
@@ -248,20 +259,18 @@ export class AdminResolver {
creations = updateCreations(creations, pendingCreationToUpdate)
}
- if (!isCreationValid(creations, amount, creationDateObj)) {
- throw new Error('Creation is not valid')
- }
- pendingCreationToUpdate.amount = BigInt(amount)
+ // all possible cases not to be true are thrown in this function
+ isCreationValid(creations, amount, creationDateObj)
+ pendingCreationToUpdate.amount = amount
pendingCreationToUpdate.memo = memo
pendingCreationToUpdate.date = new Date(creationDate)
- pendingCreationToUpdate.moderator = moderator
+ pendingCreationToUpdate.moderator = moderator.id
await AdminPendingCreation.save(pendingCreationToUpdate)
const result = new UpdatePendingCreation()
- result.amount = parseInt(amount.toString())
+ result.amount = amount
result.memo = pendingCreationToUpdate.memo
result.date = pendingCreationToUpdate.date
- result.moderator = pendingCreationToUpdate.moderator
result.creation = await getUserCreation(user.id)
@@ -286,7 +295,7 @@ export class AdminResolver {
return {
...pendingCreation,
- amount: Number(pendingCreation.amount.toString()),
+ amount: pendingCreation.amount,
firstName: user ? user.firstName : '',
lastName: user ? user.lastName : '',
email: user ? user.email : '',
@@ -298,8 +307,11 @@ export class AdminResolver {
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
@Mutation(() => Boolean)
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise {
- const entity = await AdminPendingCreation.findOneOrFail(id)
- const res = await AdminPendingCreation.delete(entity)
+ const pendingCreation = await AdminPendingCreation.findOne(id)
+ if (!pendingCreation) {
+ throw new Error('Creation not found for given id.')
+ }
+ const res = await AdminPendingCreation.delete(pendingCreation)
return !!res
}
@@ -309,7 +321,10 @@ export class AdminResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise {
- const pendingCreation = await AdminPendingCreation.findOneOrFail(id)
+ const pendingCreation = await AdminPendingCreation.findOne(id)
+ if (!pendingCreation) {
+ throw new Error('Creation not found to given id.')
+ }
const moderatorUser = getUser(context)
if (moderatorUser.id === pendingCreation.userId)
throw new Error('Moderator can not confirm own pending creation')
@@ -318,7 +333,7 @@ export class AdminResolver {
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
const creations = await getUserCreation(pendingCreation.userId, false)
- if (!isCreationValid(creations, Number(pendingCreation.amount), pendingCreation.date)) {
+ if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) {
throw new Error('Creation is not valid!!')
}
@@ -333,16 +348,14 @@ export class AdminResolver {
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
newBalance = decay.balance
}
- // TODO pending creations decimal
- newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)).toString())
+ newBalance = newBalance.add(pendingCreation.amount.toString())
const transaction = new DbTransaction()
transaction.typeId = TransactionTypeId.CREATION
transaction.memo = pendingCreation.memo
transaction.userId = pendingCreation.userId
transaction.previous = lastTransaction ? lastTransaction.id : null
- // TODO pending creations decimal
- transaction.amount = new Decimal(Number(pendingCreation.amount))
+ transaction.amount = pendingCreation.amount
transaction.creationDate = pendingCreation.date
transaction.balance = newBalance
transaction.balanceDate = receivedCallDate
@@ -448,10 +461,10 @@ export class AdminResolver {
interface CreationMap {
id: number
- creations: number[]
+ creations: Decimal[]
}
-async function getUserCreation(id: number, includePending = true): Promise {
+async function getUserCreation(id: number, includePending = true): Promise {
const creations = await getUserCreations([id], includePending)
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
}
@@ -493,32 +506,32 @@ async function getUserCreations(ids: number[], includePending = true): Promise
parseInt(raw.month) === month && parseInt(raw.id) === id,
)
- return MAX_CREATION_AMOUNT - (creation ? Number(creation.sum) : 0)
+ return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
}),
}
})
}
-function updateCreations(creations: number[], pendingCreation: AdminPendingCreation): number[] {
+function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] {
const index = getCreationIndex(pendingCreation.date.getMonth())
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
- creations[index] += parseInt(pendingCreation.amount.toString())
+ creations[index] = creations[index].plus(pendingCreation.amount.toString())
return creations
}
-function isCreationValid(creations: number[], amount: number, creationDate: Date) {
+function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
const index = getCreationIndex(creationDate.getMonth())
if (index < 0) {
- throw new Error(`No Creation found!`)
+ throw new Error('No information for available creations for the given date')
}
- if (amount > creations[index]) {
+ if (amount.greaterThan(creations[index].toString())) {
throw new Error(
- `The amount (${amount} GDD) to be created exceeds the available amount (${creations[index]} GDD) for this month.`,
+ `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
)
}
diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts
index 07b8e59e2..c658476a4 100644
--- a/backend/src/graphql/resolver/UserResolver.test.ts
+++ b/backend/src/graphql/resolver/UserResolver.test.ts
@@ -1,17 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
+import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
-import { createUser, setPassword } from '@/seeds/graphql/mutations'
-import { login, logout } from '@/seeds/graphql/queries'
+import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
+import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
import { User } from '@entity/User'
import CONFIG from '@/config'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
-import { printTimeDuration } from './UserResolver'
+import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
+import { printTimeDuration, activationLink } from './UserResolver'
// import { klicktippSignIn } from '@/apis/KlicktippController'
@@ -22,6 +23,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
}
})
+jest.mock('@/mailer/sendResetPasswordEmail', () => {
+ return {
+ __esModule: true,
+ sendResetPasswordEmail: jest.fn(),
+ }
+})
+
/*
jest.mock('@/apis/KlicktippController', () => {
return {
@@ -85,7 +93,7 @@ describe('UserResolver', () => {
})
describe('filling all tables', () => {
- it('saves the user in login_user table', () => {
+ it('saves the user in users table', () => {
expect(user).toEqual([
{
id: expect.any(Number),
@@ -100,6 +108,7 @@ describe('UserResolver', () => {
emailChecked: false,
passphrase: expect.any(String),
language: 'de',
+ isAdmin: null,
deletedAt: null,
publisherId: 1234,
referrerId: null,
@@ -336,7 +345,7 @@ describe('UserResolver', () => {
firstName: 'Bibi',
hasElopage: false,
id: expect.any(Number),
- isAdmin: false,
+ isAdmin: null,
klickTipp: {
newsletterState: false,
},
@@ -412,6 +421,356 @@ describe('UserResolver', () => {
})
})
})
+
+ describe('verifyLogin', () => {
+ describe('unauthenticated', () => {
+ it('throws an error', async () => {
+ resetToken()
+ await expect(query({ query: verifyLogin })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('user exists but is not logged in', () => {
+ beforeAll(async () => {
+ await userFactory(testEnv, bibiBloxberg)
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ })
+
+ it('throws an error', async () => {
+ resetToken()
+ await expect(query({ query: verifyLogin })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+
+ describe('authenticated', () => {
+ const variables = {
+ email: 'bibi@bloxberg.de',
+ password: 'Aa12345_',
+ }
+
+ beforeAll(async () => {
+ await query({ query: login, variables })
+ })
+
+ afterAll(() => {
+ resetToken()
+ })
+
+ it('returns user object', async () => {
+ await expect(query({ query: verifyLogin })).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ verifyLogin: {
+ email: 'bibi@bloxberg.de',
+ firstName: 'Bibi',
+ lastName: 'Bloxberg',
+ language: 'de',
+ coinanimation: true,
+ klickTipp: {
+ newsletterState: false,
+ },
+ hasElopage: false,
+ publisherId: 1234,
+ isAdmin: null,
+ },
+ },
+ }),
+ )
+ })
+ })
+ })
+ })
+
+ describe('forgotPassword', () => {
+ const variables = { email: 'bibi@bloxberg.de' }
+ describe('user is not in DB', () => {
+ it('returns true', async () => {
+ await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ forgotPassword: true,
+ },
+ }),
+ )
+ })
+ })
+
+ describe('user exists in DB', () => {
+ let result: any
+ let loginEmailOptIn: LoginEmailOptIn[]
+
+ beforeAll(async () => {
+ await userFactory(testEnv, bibiBloxberg)
+ await resetEntity(LoginEmailOptIn)
+ result = await mutate({ mutation: forgotPassword, variables })
+ loginEmailOptIn = await LoginEmailOptIn.find()
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ })
+
+ it('returns true', async () => {
+ await expect(result).toEqual(
+ expect.objectContaining({
+ data: {
+ forgotPassword: true,
+ },
+ }),
+ )
+ })
+
+ it('sends reset password email', () => {
+ expect(sendResetPasswordEmail).toBeCalledWith({
+ link: activationLink(loginEmailOptIn[0]),
+ firstName: 'Bibi',
+ lastName: 'Bloxberg',
+ email: 'bibi@bloxberg.de',
+ duration: expect.any(String),
+ })
+ })
+
+ describe('request reset password again', () => {
+ it('thows an error', async () => {
+ await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
+ }),
+ )
+ })
+ })
+ })
+ })
+
+ describe('queryOptIn', () => {
+ let loginEmailOptIn: LoginEmailOptIn[]
+
+ beforeAll(async () => {
+ await userFactory(testEnv, bibiBloxberg)
+ loginEmailOptIn = await LoginEmailOptIn.find()
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ })
+
+ describe('wrong optin code', () => {
+ it('throws an error', async () => {
+ await expect(
+ query({ query: queryOptIn, variables: { optIn: 'not-valid' } }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ // keep Whitspace in error message!
+ new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
+ "verificationCode": "not-valid"
+}`),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('correct optin code', () => {
+ it('returns true', async () => {
+ await expect(
+ query({
+ query: queryOptIn,
+ variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ queryOptIn: true,
+ },
+ }),
+ )
+ })
+ })
+ })
+
+ describe('updateUserInfos', () => {
+ describe('unauthenticated', () => {
+ it('throws an error', async () => {
+ resetToken()
+ await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('authenticated', () => {
+ beforeAll(async () => {
+ await userFactory(testEnv, bibiBloxberg)
+ await query({
+ query: login,
+ variables: {
+ email: 'bibi@bloxberg.de',
+ password: 'Aa12345_',
+ },
+ })
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ })
+
+ it('returns true', async () => {
+ await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ updateUserInfos: true,
+ },
+ }),
+ )
+ })
+
+ describe('first-name, last-name and language', () => {
+ it('updates the fields in DB', async () => {
+ await mutate({
+ mutation: updateUserInfos,
+ variables: {
+ firstName: 'Benjamin',
+ lastName: 'Blümchen',
+ locale: 'en',
+ },
+ })
+ await expect(User.findOne()).resolves.toEqual(
+ expect.objectContaining({
+ firstName: 'Benjamin',
+ lastName: 'Blümchen',
+ language: 'en',
+ }),
+ )
+ })
+ })
+
+ describe('language is not valid', () => {
+ it('thows an error', async () => {
+ await expect(
+ mutate({
+ mutation: updateUserInfos,
+ variables: {
+ locale: 'not-valid',
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError(`"not-valid" isn't a valid language`)],
+ }),
+ )
+ })
+ })
+
+ describe('password', () => {
+ describe('wrong old password', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updateUserInfos,
+ variables: {
+ password: 'wrong password',
+ passwordNew: 'Aa12345_',
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Old password is invalid')],
+ }),
+ )
+ })
+ })
+
+ describe('invalid new password', () => {
+ it('throws an error', async () => {
+ await expect(
+ mutate({
+ mutation: updateUserInfos,
+ variables: {
+ password: 'Aa12345_',
+ passwordNew: 'Aa12345',
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [
+ new GraphQLError(
+ 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
+ ),
+ ],
+ }),
+ )
+ })
+ })
+
+ describe('correct old and new password', () => {
+ it('returns true', async () => {
+ await expect(
+ mutate({
+ mutation: updateUserInfos,
+ variables: {
+ password: 'Aa12345_',
+ passwordNew: 'Bb12345_',
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: { updateUserInfos: true },
+ }),
+ )
+ })
+
+ it('can login wtih new password', async () => {
+ await expect(
+ query({
+ query: login,
+ variables: {
+ email: 'bibi@bloxberg.de',
+ password: 'Bb12345_',
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ login: expect.objectContaining({
+ email: 'bibi@bloxberg.de',
+ }),
+ },
+ }),
+ )
+ })
+
+ it('cannot login wtih old password', async () => {
+ await expect(
+ query({
+ query: login,
+ variables: {
+ email: 'bibi@bloxberg.de',
+ password: 'Aa12345_',
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('No user with this credentials')],
+ }),
+ )
+ })
+ })
+ })
+ })
+ })
})
describe('printTimeDuration', () => {
diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts
index 137c09622..4ab5a901b 100644
--- a/backend/src/graphql/resolver/UserResolver.ts
+++ b/backend/src/graphql/resolver/UserResolver.ts
@@ -19,9 +19,7 @@ import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
-import { ROLE_ADMIN } from '@/auth/ROLES'
import { hasElopageBuys } from '@/util/hasElopageBuys'
-import { ServerUser } from '@entity/ServerUser'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native')
@@ -207,7 +205,6 @@ export class UserResolver {
})
user.coinanimation = coinanimation
- user.isAdmin = context.role === ROLE_ADMIN
return user
}
@@ -243,16 +240,11 @@ export class UserResolver {
}
const user = new User(dbUser)
- // user.email = email
- // user.pubkey = dbUser.pubKey.toString('hex')
- user.language = dbUser.language
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
if (!user.hasElopage && publisherId) {
user.publisherId = publisherId
- // TODO: Check if we can use updateUserInfos
- // await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
dbUser.publisherId = publisherId
DbUser.save(dbUser)
}
@@ -266,10 +258,6 @@ export class UserResolver {
})
user.coinanimation = coinanimation
- // context.role is not set to the actual role yet on login
- const countServerUsers = await ServerUser.count({ email: user.email })
- user.isAdmin = countServerUsers > 0
-
context.setHeaders.push({
key: 'token',
value: encode(dbUser.pubKey),
@@ -529,15 +517,7 @@ export class UserResolver {
@Mutation(() => Boolean)
async updateUserInfos(
@Args()
- {
- firstName,
- lastName,
- language,
- publisherId,
- password,
- passwordNew,
- coinanimation,
- }: UpdateUserInfosArgs,
+ { firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs,
@Ctx() context: Context,
): Promise {
const userEntity = getUser(context)
@@ -581,11 +561,6 @@ export class UserResolver {
userEntity.privKey = encryptedPrivkey
}
- // Save publisherId only if Elopage is not yet registered
- if (publisherId && !(await this.hasElopage(context))) {
- userEntity.publisherId = publisherId
- }
-
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts
index 64f693360..e49be3758 100644
--- a/backend/src/seeds/factory/creation.ts
+++ b/backend/src/seeds/factory/creation.ts
@@ -17,27 +17,22 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
export const creationFactory = async (
client: ApolloServerTestClient,
creation: CreationInterface,
-): Promise => {
+): Promise => {
const { mutate, query } = client
- // login as Peter Lustig (admin) and get his user ID
- const {
- data: {
- login: { id },
- },
- } = await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
+ await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
- await mutate({ mutation: createPendingCreation, variables: { ...creation, moderator: id } })
+ // TODO it would be nice to have this mutation return the id
+ await mutate({ mutation: createPendingCreation, variables: { ...creation } })
- // get User
const user = await User.findOneOrFail({ where: { email: creation.email } })
- if (creation.confirmed) {
- const pendingCreation = await AdminPendingCreation.findOneOrFail({
- where: { userId: user.id },
- order: { created: 'DESC' },
- })
+ const pendingCreation = await AdminPendingCreation.findOneOrFail({
+ where: { userId: user.id, amount: creation.amount },
+ order: { created: 'DESC' },
+ })
+ if (creation.confirmed) {
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
if (creation.moveCreationDate) {
@@ -55,5 +50,7 @@ export const creationFactory = async (
await transaction.save()
}
}
+ } else {
+ return pendingCreation
}
}
diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts
index ff4c1d6c9..d94f94b3c 100644
--- a/backend/src/seeds/factory/user.ts
+++ b/backend/src/seeds/factory/user.ts
@@ -1,14 +1,13 @@
import { createUser, setPassword } from '@/seeds/graphql/mutations'
import { User } from '@entity/User'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
-import { ServerUser } from '@entity/ServerUser'
import { UserInterface } from '@/seeds/users/UserInterface'
import { ApolloServerTestClient } from 'apollo-server-testing'
export const userFactory = async (
client: ApolloServerTestClient,
user: UserInterface,
-): Promise => {
+): Promise => {
const { mutate } = client
const {
@@ -25,27 +24,15 @@ export const userFactory = async (
})
}
+ // get user from database
+ const dbUser = await User.findOneOrFail({ id })
+
if (user.createdAt || user.deletedAt || user.isAdmin) {
- // get user from database
- const dbUser = await User.findOneOrFail({ id })
-
- if (user.createdAt || user.deletedAt) {
- if (user.createdAt) dbUser.createdAt = user.createdAt
- if (user.deletedAt) dbUser.deletedAt = user.deletedAt
- await dbUser.save()
- }
-
- if (user.isAdmin) {
- const admin = new ServerUser()
- admin.username = dbUser.firstName
- admin.password = 'please_refactor'
- admin.email = dbUser.email
- admin.role = 'admin'
- admin.activated = 1
- admin.lastLogin = new Date()
- admin.created = dbUser.createdAt
- admin.modified = dbUser.createdAt
- await admin.save()
- }
+ if (user.createdAt) dbUser.createdAt = user.createdAt
+ if (user.deletedAt) dbUser.deletedAt = user.deletedAt
+ if (user.isAdmin) dbUser.isAdmin = new Date()
+ await dbUser.save()
}
+
+ return dbUser
}
diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts
index 298d56bdb..d3026dbdd 100644
--- a/backend/src/seeds/graphql/mutations.ts
+++ b/backend/src/seeds/graphql/mutations.ts
@@ -18,6 +18,12 @@ export const setPassword = gql`
}
`
+export const forgotPassword = gql`
+ mutation ($email: String!) {
+ forgotPassword(email: $email)
+ }
+`
+
export const updateUserInfos = gql`
mutation (
$firstName: String
@@ -78,20 +84,8 @@ export const createTransactionLink = gql`
// from admin interface
export const createPendingCreation = gql`
- mutation (
- $email: String!
- $amount: Float!
- $memo: String!
- $creationDate: String!
- $moderator: Int!
- ) {
- createPendingCreation(
- email: $email
- amount: $amount
- memo: $memo
- creationDate: $creationDate
- moderator: $moderator
- )
+ mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
+ createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
}
`
@@ -100,3 +94,48 @@ export const confirmPendingCreation = gql`
confirmPendingCreation(id: $id)
}
`
+
+export const deleteUser = gql`
+ mutation ($userId: Int!) {
+ deleteUser(userId: $userId)
+ }
+`
+
+export const unDeleteUser = gql`
+ mutation ($userId: Int!) {
+ unDeleteUser(userId: $userId)
+ }
+`
+
+export const createPendingCreations = gql`
+ mutation ($pendingCreations: [CreatePendingCreationArgs!]!) {
+ createPendingCreations(pendingCreations: $pendingCreations) {
+ success
+ successfulCreation
+ failedCreation
+ }
+ }
+`
+
+export const updatePendingCreation = gql`
+ mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
+ updatePendingCreation(
+ id: $id
+ email: $email
+ amount: $amount
+ memo: $memo
+ creationDate: $creationDate
+ ) {
+ amount
+ date
+ memo
+ creation
+ }
+ }
+`
+
+export const deletePendingCreation = gql`
+ mutation ($id: Int!) {
+ deletePendingCreation(id: $id)
+ }
+`
diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts
index 11a675eeb..82067c968 100644
--- a/backend/src/seeds/graphql/queries.ts
+++ b/backend/src/seeds/graphql/queries.ts
@@ -43,6 +43,12 @@ export const logout = gql`
}
`
+export const queryOptIn = gql`
+ query ($optIn: String!) {
+ queryOptIn(optIn: $optIn)
+ }
+`
+
export const transactionsQuery = gql`
query (
$currentPage: Int = 1
@@ -142,3 +148,21 @@ export const queryTransactionLink = gql`
}
}
`
+
+// from admin interface
+
+export const getPendingCreations = gql`
+ query {
+ getPendingCreations {
+ id
+ firstName
+ lastName
+ email
+ amount
+ memo
+ date
+ moderator
+ creation
+ }
+ }
+`
diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts
index 37c9992a7..f26000e06 100644
--- a/backend/src/seeds/index.ts
+++ b/backend/src/seeds/index.ts
@@ -4,7 +4,7 @@
import createServer from '../server/createServer'
import { createTestClient } from 'apollo-server-testing'
-import { name, internet, random } from 'faker'
+import { name, internet, datatype } from 'faker'
import { users } from './users/index'
import { creations } from './creation/index'
@@ -13,6 +13,9 @@ import { userFactory } from './factory/user'
import { creationFactory } from './factory/creation'
import { transactionLinkFactory } from './factory/transactionLink'
import { entities } from '@entity/index'
+import CONFIG from '@/config'
+
+CONFIG.EMAIL = false
const context = {
token: '',
@@ -57,7 +60,7 @@ const run = async () => {
firstName: name.firstName(),
lastName: name.lastName(),
email: internet.email(),
- language: random.boolean() ? 'en' : 'de',
+ language: datatype.boolean() ? 'en' : 'de',
})
}
diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts
index 33ac2fad2..0d0d12f6c 100644
--- a/backend/src/util/communityUser.ts
+++ b/backend/src/util/communityUser.ts
@@ -17,6 +17,7 @@ const communityDbUser: dbUser = {
createdAt: new Date(),
emailChecked: false,
language: '',
+ isAdmin: null,
publisherId: 0,
passphrase: '',
settings: [],
diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts
index 8c1aec65b..08d44b48d 100644
--- a/backend/src/util/virtualTransactions.ts
+++ b/backend/src/util/virtualTransactions.ts
@@ -70,6 +70,7 @@ const virtualDecayTransaction = (
typeId: TransactionTypeId.DECAY,
amount: decay.decay ? decay.roundedDecay : new Decimal(0),
balance: decay.balance
+ .toDecimalPlaces(2, Decimal.ROUND_DOWN)
.minus(holdAvailabeAmount.toString())
.toDecimalPlaces(2, Decimal.ROUND_DOWN),
balanceDate: time,
diff --git a/database/README.md b/database/README.md
index 84db3d194..d6cf84518 100644
--- a/database/README.md
+++ b/database/README.md
@@ -30,4 +30,3 @@ yarn dev_down
yarn dev_reset
```
Runs all down migrations and after this all up migrations.
-
diff --git a/database/entity/0034-drop_server_user_table/User.ts b/database/entity/0034-drop_server_user_table/User.ts
new file mode 100644
index 000000000..1f56d13d2
--- /dev/null
+++ b/database/entity/0034-drop_server_user_table/User.ts
@@ -0,0 +1,81 @@
+import {
+ BaseEntity,
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ OneToMany,
+ DeleteDateColumn,
+} from 'typeorm'
+import { UserSetting } from '../UserSetting'
+
+@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
+export class User extends BaseEntity {
+ @PrimaryGeneratedColumn('increment', { unsigned: true })
+ id: 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
+
+ @DeleteDateColumn()
+ deletedAt: Date | null
+
+ @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({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
+ language: string
+
+ @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
+ isAdmin: Date | null
+
+ @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
+ referrerId?: number | null
+
+ @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/0035-admin_pending_creations_decimal/AdminPendingCreation.ts b/database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts
new file mode 100644
index 000000000..3cd83a3a5
--- /dev/null
+++ b/database/entity/0035-admin_pending_creations_decimal/AdminPendingCreation.ts
@@ -0,0 +1,33 @@
+import Decimal from 'decimal.js-light'
+import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm'
+import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
+
+@Entity('admin_pending_creations')
+export class AdminPendingCreation extends BaseEntity {
+ @PrimaryGeneratedColumn('increment', { unsigned: true })
+ id: number
+
+ @Column({ unsigned: true, nullable: false })
+ userId: number
+
+ @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
+ created: Date
+
+ @Column({ type: 'datetime', nullable: false })
+ date: Date
+
+ @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
+ memo: string
+
+ @Column({
+ type: 'decimal',
+ precision: 40,
+ scale: 20,
+ nullable: false,
+ transformer: DecimalTransformer,
+ })
+ amount: Decimal
+
+ @Column()
+ moderator: number
+}
diff --git a/database/entity/AdminPendingCreation.ts b/database/entity/AdminPendingCreation.ts
index 03eeab883..b2b37d7c4 100644
--- a/database/entity/AdminPendingCreation.ts
+++ b/database/entity/AdminPendingCreation.ts
@@ -1 +1 @@
-export { AdminPendingCreation } from './0015-admin_pending_creations/AdminPendingCreation'
+export { AdminPendingCreation } from './0035-admin_pending_creations_decimal/AdminPendingCreation'
diff --git a/database/entity/ServerUser.ts b/database/entity/ServerUser.ts
deleted file mode 100644
index 495513823..000000000
--- a/database/entity/ServerUser.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { ServerUser } from './0001-init_db/ServerUser'
diff --git a/database/entity/User.ts b/database/entity/User.ts
index 35dfb7bbe..4cd68174c 100644
--- a/database/entity/User.ts
+++ b/database/entity/User.ts
@@ -1 +1 @@
-export { User } from './0033-add_referrer_id/User'
+export { User } from './0034-drop_server_user_table/User'
diff --git a/database/entity/index.ts b/database/entity/index.ts
index cb6f56ab0..542333755 100644
--- a/database/entity/index.ts
+++ b/database/entity/index.ts
@@ -1,7 +1,6 @@
import { LoginElopageBuys } from './LoginElopageBuys'
import { LoginEmailOptIn } from './LoginEmailOptIn'
import { Migration } from './Migration'
-import { ServerUser } from './ServerUser'
import { Transaction } from './Transaction'
import { TransactionLink } from './TransactionLink'
import { User } from './User'
@@ -13,7 +12,6 @@ export const entities = [
LoginElopageBuys,
LoginEmailOptIn,
Migration,
- ServerUser,
Transaction,
TransactionLink,
User,
diff --git a/database/migrations/0034-drop_server_user_table.ts b/database/migrations/0034-drop_server_user_table.ts
new file mode 100644
index 000000000..be6b44489
--- /dev/null
+++ b/database/migrations/0034-drop_server_user_table.ts
@@ -0,0 +1,37 @@
+/* MIGRATION DROP server_users TABLE
+add isAdmin COLUMN to users TABLE */
+
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn('ALTER TABLE `users` ADD COLUMN `is_admin` datetime DEFAULT NULL AFTER `language`;')
+
+ await queryFn(
+ 'UPDATE users AS users INNER JOIN server_users AS server_users ON users.email = server_users.email SET users.is_admin = server_users.modified WHERE users.email IN (SELECT email from server_users);',
+ )
+
+ await queryFn('DROP TABLE `server_users`;')
+}
+
+export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn(`
+ CREATE TABLE IF NOT EXISTS \`server_users\` (
+ \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
+ \`username\` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ \`password\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ \`email\` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ \`role\` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
+ \`activated\` tinyint(4) NOT NULL DEFAULT '0',
+ \`last_login\` datetime DEFAULT NULL,
+ \`created\` datetime NOT NULL,
+ \`modified\` datetime NOT NULL,
+ PRIMARY KEY (\`id\`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`)
+
+ await queryFn(
+ 'INSERT INTO `server_users` (`email`, `username`, `password`, `created`, `modified`) SELECT `email`, `first_name`, `password`, `is_admin`, `is_admin` FROM `users` WHERE `is_admin` IS NOT NULL;',
+ )
+
+ await queryFn('ALTER TABLE `users` DROP COLUMN `is_admin`;')
+}
diff --git a/database/migrations/0035-admin_pending_creations_decimal.ts b/database/migrations/0035-admin_pending_creations_decimal.ts
new file mode 100644
index 000000000..d3648f376
--- /dev/null
+++ b/database/migrations/0035-admin_pending_creations_decimal.ts
@@ -0,0 +1,42 @@
+/* MIGRATION TO CHANGE SEVERAL FIELDS ON `admin_pending_creations`
+ * - `amount` FIELD TYPE TO `Decimal`
+ * - `memo` FIELD TYPE TO `varchar(255)`, collate `utf8mb4_unicode_ci`
+ */
+
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ // rename `amount` to `amount_bigint`
+ await queryFn('ALTER TABLE `admin_pending_creations` RENAME COLUMN `amount` TO `amount_bigint`;')
+ // add `amount` (decimal)
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount` DECIMAL(40,20) DEFAULT NULL AFTER `amount_bigint`;',
+ )
+ // fill new `amount` column
+ await queryFn('UPDATE `admin_pending_creations` SET `amount` = `amount_bigint` DIV 10000;')
+ // make `amount` not nullable
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `amount` DECIMAL(40,20) NOT NULL;',
+ )
+ // drop `amount_bitint` column
+ await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount_bigint`;')
+
+ // change `memo` to varchar(255), collate utf8mb4_unicode_ci
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `memo` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL;',
+ )
+}
+
+export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn('ALTER TABLE `admin_pending_creations` MODIFY COLUMN `memo` text DEFAULT NULL;')
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount_bigint` bigint(20) DEFAULT NULL AFTER `amount`;',
+ )
+ await queryFn('UPDATE `admin_pending_creations` SET `amount_bigint` = `amount` * 10000;')
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` MODIFY COLUMN `amount_bigint` bigint(20) NOT NULL;',
+ )
+ await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN `amount`;')
+ await queryFn('ALTER TABLE `admin_pending_creations` RENAME COLUMN `amount_bigint` TO `amount`;')
+}
diff --git a/database/package.json b/database/package.json
index a1fffa882..a19cc57da 100644
--- a/database/package.json
+++ b/database/package.json
@@ -1,6 +1,6 @@
{
"name": "gradido-database",
- "version": "1.7.1",
+ "version": "1.8.1",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",
diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist
index 316fb60c2..a1751a859 100644
--- a/deployment/bare_metal/.env.dist
+++ b/deployment/bare_metal/.env.dist
@@ -21,6 +21,7 @@ WEBHOOK_GITHUB_BRANCH=master
COMMUNITY_NAME="Gradido Development Stage1"
COMMUNITY_URL=https://stage1.gradido.net/
COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
+COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
# backend
diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh
index 95b89241f..b608952c0 100755
--- a/deployment/bare_metal/start.sh
+++ b/deployment/bare_metal/start.sh
@@ -105,7 +105,8 @@ yarn install
yarn build
if [ "$DEPLOY_SEED_DATA" = "true" ]; then
yarn dev_up
- yarn dev_reset
+# As dev_reset is not running properly (0019-replace_login_user_id_with_state_user_id)
+# yarn dev_reset
else
yarn up
fi
diff --git a/frontend/package.json b/frontend/package.json
index f54f85527..677bb6e1d 100755
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
- "version": "1.7.1",
+ "version": "1.8.1",
"private": true,
"scripts": {
"start": "node run/server.js",
@@ -25,6 +25,7 @@
"babel-preset-vue": "^2.0.2",
"bootstrap": "^4.5.3",
"bootstrap-vue": "^2.21.2",
+ "clipboard-polyfill": "^4.0.0-rc1",
"es6-promise": "^4.1.1",
"eslint": "^7.25.0",
"eslint-config-prettier": "^8.1.0",
diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue
index 936f6db1a..7a6cf0ec1 100644
--- a/frontend/src/components/ClipboardCopy.vue
+++ b/frontend/src/components/ClipboardCopy.vue
@@ -1,6 +1,6 @@
-
+
@@ -11,6 +11,10 @@
+
+
{{ $t('gdd_per_link.not-copied') }}
+
{{ link }}
+