diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 3d046fcda..bb2441701 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: 58
+ min_coverage: 66
token: ${{ github.token }}
##########################################################################
diff --git a/.gitignore b/.gitignore
index 32e11f545..08ccd2b30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,9 @@ package-lock.json
/deployment/bare_metal/nginx/update-page/updating.html
/deployment/bare_metal/log
/deployment/bare_metal/backup
+
+# Node Version Manager configuration file
+.nvmrc
+
+# Apple macOS folder attribute file
+.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d8b71dac..48eeff9a9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,8 +4,44 @@ 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.3](https://github.com/gradido/gradido/compare/1.8.2...1.8.3)
+
+- Checkbox [`#1894`](https://github.com/gradido/gradido/pull/1894)
+- fix: Count Deprecated Links as Well [`#1892`](https://github.com/gradido/gradido/pull/1892)
+
+#### [1.8.2](https://github.com/gradido/gradido/compare/1.8.1...1.8.2)
+
+> 12 May 2022
+
+- Release 1.8.2 [`#1890`](https://github.com/gradido/gradido/pull/1890)
+- Update README.md [`#1878`](https://github.com/gradido/gradido/pull/1878)
+- fix: Unique Previous Column in Transactions Table [`#1879`](https://github.com/gradido/gradido/pull/1879)
+- fix: Up and Down Migrations for Older SQL Versions [`#1861`](https://github.com/gradido/gradido/pull/1861)
+- 🍰 Refactor THX Page – 1. Step [`#1856`](https://github.com/gradido/gradido/pull/1856)
+- Create LICENSE [`#1803`](https://github.com/gradido/gradido/pull/1803)
+- docu: Update Deployment Documentation [`#1864`](https://github.com/gradido/gradido/pull/1864)
+- fix: Loading Transaction Links after Reopening Link List [`#1863`](https://github.com/gradido/gradido/pull/1863)
+- 🍰 Add NVM Config Files To '.gitignore' [`#1846`](https://github.com/gradido/gradido/pull/1846)
+
+#### [1.8.1](https://github.com/gradido/gradido/compare/1.8.0...1.8.1)
+
+> 28 April 2022
+
+- v1.8.1 [`#1855`](https://github.com/gradido/gradido/pull/1855)
+- 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)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 8bfff0f25..289a39109 100644
--- a/README.md
+++ b/README.md
@@ -131,3 +131,15 @@ Note: The Changelog will be regenerated with all tags on release on the external
## Useful Links
- [Gradido.net](https://gradido.net/)
+
+
+## Attributions
+
+
+
+Browser compatibility testing with [BrowserStack](https://www.browserstack.com/).
+
+
+## License
+See the [LICENSE](LICENSE.md) file for license rights and limitations (Apache-2.0 license).
+
diff --git a/admin/.gitignore b/admin/.gitignore
index 6bb62f667..a67d270bc 100644
--- a/admin/.gitignore
+++ b/admin/.gitignore
@@ -9,4 +9,4 @@ dist/
coverage/
# emacs
-*~
\ No newline at end of file
+*~
diff --git a/admin/.prettierrc.js b/admin/.prettierrc.js
index e88113754..bc1d767d7 100644
--- a/admin/.prettierrc.js
+++ b/admin/.prettierrc.js
@@ -4,5 +4,6 @@ module.exports = {
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
- bracketSpacing: true
+ bracketSpacing: true,
+ endOfLine: "auto",
};
diff --git a/admin/package.json b/admin/package.json
index c5b2e60f5..57711b8be 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.8.0",
+ "version": "1.8.3",
"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 05402ed9f..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: Decimal!
- $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..5740e24cc 100644
--- a/admin/src/graphql/searchUsers.js
+++ b/admin/src/graphql/searchUsers.js
@@ -5,15 +5,13 @@ export const searchUsers = gql`
$searchText: String!
$currentPage: Int
$pageSize: Int
- $notActivated: Boolean
- $isDeleted: Boolean
+ $filters: SearchUsersFiltersInput
) {
searchUsers(
searchText: $searchText
currentPage: $currentPage
pageSize: $pageSize
- notActivated: $notActivated
- isDeleted: $isDeleted
+ filters: $filters
) {
userCount
userList {
diff --git a/admin/src/graphql/updatePendingCreation.js b/admin/src/graphql/updatePendingCreation.js
index cd0ae6c8e..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: Decimal!
- $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..432cbe19b 100644
--- a/admin/src/pages/Creation.spec.js
+++ b/admin/src/pages/Creation.spec.js
@@ -71,8 +71,10 @@ describe('Creation', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filters: {
+ filterByActivated: true,
+ filterByDeleted: false,
+ },
},
}),
)
@@ -271,8 +273,10 @@ describe('Creation', () => {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filters: {
+ filterByActivated: true,
+ filterByDeleted: false,
+ },
},
}),
)
@@ -288,8 +292,10 @@ describe('Creation', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filters: {
+ filterByActivated: true,
+ filterByDeleted: false,
+ },
},
}),
)
@@ -305,8 +311,10 @@ describe('Creation', () => {
searchText: '',
currentPage: 2,
pageSize: 25,
- isDeleted: false,
- notActivated: false,
+ filters: {
+ filterByActivated: true,
+ filterByDeleted: false,
+ },
},
}),
)
diff --git a/admin/src/pages/Creation.vue b/admin/src/pages/Creation.vue
index e5b93350f..17962bfff 100644
--- a/admin/src/pages/Creation.vue
+++ b/admin/src/pages/Creation.vue
@@ -102,8 +102,10 @@ export default {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
- notActivated: false,
- isDeleted: false,
+ filters: {
+ 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..a1d809a66 100644
--- a/admin/src/pages/UserSearch.spec.js
+++ b/admin/src/pages/UserSearch.spec.js
@@ -7,7 +7,7 @@ const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: {
- userCount: 1,
+ userCount: 4,
userList: [
{
userId: 1,
@@ -82,8 +82,10 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: null,
+ },
},
}),
)
@@ -101,8 +103,10 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: true,
- isDeleted: null,
+ filters: {
+ filterByActivated: false,
+ filterByDeleted: null,
+ },
},
}),
)
@@ -121,8 +125,10 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: true,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: true,
+ },
},
}),
)
@@ -141,8 +147,10 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 2,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: null,
+ },
},
}),
)
@@ -161,8 +169,10 @@ describe('UserSearch', () => {
searchText: 'search string',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: null,
+ },
},
}),
)
@@ -178,8 +188,10 @@ describe('UserSearch', () => {
searchText: '',
currentPage: 1,
pageSize: 25,
- notActivated: null,
- isDeleted: null,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: null,
+ },
},
}),
)
diff --git a/admin/src/pages/UserSearch.vue b/admin/src/pages/UserSearch.vue
index b2737bae6..7b638c316 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')
+ : ''
+ }}
{{ $t('user_search') }}
@@ -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,10 @@ export default {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
- notActivated: this.filterCheckedEmails,
- isDeleted: this.filterDeletedUser,
+ filters: {
+ filterByActivated: this.filterByActivated,
+ filterByDeleted: this.filterByDeleted,
+ },
},
fetchPolicy: 'no-cache',
})
diff --git a/backend/.env.dist b/backend/.env.dist
index de33a7272..62b786456 100644
--- a/backend/.env.dist
+++ b/backend/.env.dist
@@ -49,4 +49,8 @@ EMAIL_CODE_VALID_TIME=1440
EMAIL_CODE_REQUEST_TIME=10
# Webhook
-WEBHOOK_ELOPAGE_SECRET=secret
\ No newline at end of file
+WEBHOOK_ELOPAGE_SECRET=secret
+
+# SET LOG LEVEL AS NEEDED IN YOUR .ENV
+# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
+# LOG_LEVEL=info
diff --git a/backend/.env.template b/backend/.env.template
index 8ce8fca4e..140ec67e9 100644
--- a/backend/.env.template
+++ b/backend/.env.template
@@ -47,4 +47,4 @@ EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
# Webhook
-WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
\ No newline at end of file
+WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
diff --git a/backend/.gitignore b/backend/.gitignore
index 147e82849..6eadcc884 100644
--- a/backend/.gitignore
+++ b/backend/.gitignore
@@ -5,4 +5,4 @@
package-json.lock
coverage
# emacs
-*~
\ No newline at end of file
+*~
diff --git a/backend/.prettierrc.js b/backend/.prettierrc.js
index 8495e3f20..bc1d767d7 100644
--- a/backend/.prettierrc.js
+++ b/backend/.prettierrc.js
@@ -5,4 +5,5 @@ module.exports = {
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true,
+ endOfLine: "auto",
};
diff --git a/backend/log4js-config.json b/backend/log4js-config.json
new file mode 100644
index 000000000..1c4b3fb6d
--- /dev/null
+++ b/backend/log4js-config.json
@@ -0,0 +1,66 @@
+{
+ "appenders":
+ {
+ "access":
+ {
+ "type": "dateFile",
+ "filename": "../logs/backend/access.log",
+ "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
+ "keepFileExt" : true,
+ "fileNameSep" : "_"
+ },
+ "apollo":
+ {
+ "type": "dateFile",
+ "filename": "../logs/backend/apollo.log",
+ "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
+ "keepFileExt" : true,
+ "fileNameSep" : "_"
+ },
+ "errorFile":
+ {
+ "type": "dateFile",
+ "filename": "../logs/backend/errors.log",
+ "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
+ "keepFileExt" : true,
+ "fileNameSep" : "_"
+ },
+ "errors":
+ {
+ "type": "logLevelFilter",
+ "level": "error",
+ "appender": "errorFile"
+ },
+ "out":
+ {
+ "type": "stdout",
+ "layout":
+ {
+ "type": "pattern", "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m"
+ }
+
+ }
+ },
+ "categories":
+ {
+ "default":
+ {
+ "appenders":
+ [
+ "out",
+ "apollo",
+ "errors"
+ ],
+ "level": "debug",
+ "enableCallStack": true
+ },
+ "http":
+ {
+ "appenders":
+ [
+ "access"
+ ],
+ "level": "info"
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
index 641a24601..678e3578a 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "gradido-backend",
- "version": "1.8.0",
+ "version": "1.8.3",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@@ -19,7 +19,6 @@
"dependencies": {
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
- "apollo-log": "^1.1.0",
"apollo-server-express": "^2.25.2",
"apollo-server-testing": "^2.25.2",
"axios": "^0.21.1",
@@ -33,6 +32,7 @@
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0",
+ "log4js": "^6.4.6",
"mysql2": "^2.3.0",
"nodemailer": "^6.6.5",
"random-bigint": "^0.0.1",
diff --git a/backend/src/apis/HttpRequest.ts b/backend/src/apis/HttpRequest.ts
index c1f99dc46..4039e3a98 100644
--- a/backend/src/apis/HttpRequest.ts
+++ b/backend/src/apis/HttpRequest.ts
@@ -1,10 +1,14 @@
import axios from 'axios'
+import { backendLogger as logger } from '@/server/logger'
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiPost = async (url: string, payload: unknown): Promise => {
+ logger.trace('POST: url=' + url + ' payload=' + payload)
return axios
.post(url, payload)
.then((result) => {
+ logger.trace('POST-Response: result=' + result)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
@@ -20,9 +24,11 @@ export const apiPost = async (url: string, payload: unknown): Promise => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiGet = async (url: string): Promise => {
+ logger.trace('GET: url=' + url)
return axios
.get(url)
.then((result) => {
+ logger.trace('GET-Response: result=' + result)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index 1eee1b9a4..559b8e9c5 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -10,8 +10,11 @@ Decimal.set({
})
const constants = {
- DB_VERSION: '0035-admin_pending_creations_decimal',
+ DB_VERSION: '0036-unique_previous_in_transactions',
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
+ LOG4JS_CONFIG: 'log4js-config.json',
+ // default log level on production should be info
+ LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: {
DEFAULT: 'DEFAULT',
EXPECTED: 'v6.2022-04-21',
diff --git a/backend/src/graphql/arg/CreatePendingCreationArgs.ts b/backend/src/graphql/arg/CreatePendingCreationArgs.ts
index 0cadf5e62..11c345465 100644
--- a/backend/src/graphql/arg/CreatePendingCreationArgs.ts
+++ b/backend/src/graphql/arg/CreatePendingCreationArgs.ts
@@ -1,4 +1,4 @@
-import { ArgsType, Field, InputType, Int } from 'type-graphql'
+import { ArgsType, Field, InputType } from 'type-graphql'
import Decimal from 'decimal.js-light'
@InputType()
@@ -15,7 +15,4 @@ export default class CreatePendingCreationArgs {
@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..8db6bfc06 100644
--- a/backend/src/graphql/arg/SearchUsersArgs.ts
+++ b/backend/src/graphql/arg/SearchUsersArgs.ts
@@ -1,4 +1,5 @@
import { ArgsType, Field, Int } from 'type-graphql'
+import SearchUsersFilters from '@arg/SearchUsersFilters'
@ArgsType()
export default class SearchUsersArgs {
@@ -11,9 +12,6 @@ export default class SearchUsersArgs {
@Field(() => Int, { nullable: true })
pageSize?: number
- @Field(() => Boolean, { nullable: true })
- notActivated?: boolean | null
-
- @Field(() => Boolean, { nullable: true })
- isDeleted?: boolean | null
+ @Field(() => SearchUsersFilters, { nullable: true })
+ filters: SearchUsersFilters
}
diff --git a/backend/src/graphql/arg/SearchUsersFilters.ts b/backend/src/graphql/arg/SearchUsersFilters.ts
new file mode 100644
index 000000000..de7c7c20a
--- /dev/null
+++ b/backend/src/graphql/arg/SearchUsersFilters.ts
@@ -0,0 +1,11 @@
+import { Field, InputType, ObjectType } from 'type-graphql'
+
+@ObjectType()
+@InputType('SearchUsersFiltersInput')
+export default class SearchUsersFilters {
+ @Field(() => Boolean, { nullable: true, defaultValue: null })
+ filterByActivated?: boolean | null
+
+ @Field(() => Boolean, { nullable: true, defaultValue: null })
+ filterByDeleted?: boolean | null
+}
diff --git a/backend/src/graphql/arg/TransactionLinkFilters.ts b/backend/src/graphql/arg/TransactionLinkFilters.ts
index e2f752d3f..b009a3180 100644
--- a/backend/src/graphql/arg/TransactionLinkFilters.ts
+++ b/backend/src/graphql/arg/TransactionLinkFilters.ts
@@ -3,11 +3,11 @@ import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class TransactionLinkFilters {
@Field(() => Boolean, { nullable: true, defaultValue: true })
- withDeleted?: boolean
+ filterByDeleted?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true })
- withExpired?: boolean
+ filterByExpired?: boolean
@Field(() => Boolean, { nullable: true, defaultValue: true })
- withRedeemed?: boolean
+ filterByRedeemed?: boolean
}
diff --git a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts
index 3cd85e84b..691d73154 100644
--- a/backend/src/graphql/arg/UpdatePendingCreationArgs.ts
+++ b/backend/src/graphql/arg/UpdatePendingCreationArgs.ts
@@ -17,7 +17,4 @@ export default class UpdatePendingCreationArgs {
@Field(() => String)
creationDate: string
-
- @Field(() => Int)
- moderator: number
}
diff --git a/backend/src/graphql/model/UpdatePendingCreation.ts b/backend/src/graphql/model/UpdatePendingCreation.ts
index 85d3af2cc..e19e1e064 100644
--- a/backend/src/graphql/model/UpdatePendingCreation.ts
+++ b/backend/src/graphql/model/UpdatePendingCreation.ts
@@ -12,9 +12,6 @@ export class UpdatePendingCreation {
@Field(() => Decimal)
amount: Decimal
- @Field(() => Number)
- moderator: number
-
@Field(() => [Decimal])
creation: Decimal[]
}
diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts
new file mode 100644
index 000000000..4771232ea
--- /dev/null
+++ b/backend/src/graphql/resolver/AdminResolver.test.ts
@@ -0,0 +1,1327 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+
+import { convertObjValuesToArray } from '@/util/utilities'
+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,
+ searchUsers,
+ 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('search users', () => {
+ const variablesWithoutTextAndFilters = {
+ searchText: '',
+ currentPage: 1,
+ pageSize: 25,
+ filters: null,
+ }
+
+ describe('unauthenticated', () => {
+ it('returns an error', async () => {
+ await expect(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ },
+ }),
+ ).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(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('401 Unauthorized')],
+ }),
+ )
+ })
+ })
+
+ describe('with admin rights', () => {
+ const allUsers = {
+ bibi: expect.objectContaining({
+ email: 'bibi@bloxberg.de',
+ }),
+ garrick: expect.objectContaining({
+ email: 'garrick@ollivander.com',
+ }),
+ peter: expect.objectContaining({
+ email: 'peter@lustig.de',
+ }),
+ stephen: expect.objectContaining({
+ email: 'stephen@hawking.uk',
+ }),
+ }
+
+ beforeAll(async () => {
+ admin = await userFactory(testEnv, peterLustig)
+ await query({
+ query: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+
+ await userFactory(testEnv, bibiBloxberg)
+ await userFactory(testEnv, stephenHawking)
+ await userFactory(testEnv, garrickOllivander)
+ })
+
+ afterAll(async () => {
+ await cleanDB()
+ resetToken()
+ })
+
+ describe('without any filters', () => {
+ it('finds all users', async () => {
+ await expect(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ searchUsers: {
+ userCount: 4,
+ userList: expect.arrayContaining(convertObjValuesToArray(allUsers)),
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ describe('all filters are null', () => {
+ it('finds all users', async () => {
+ await expect(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: null,
+ },
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ searchUsers: {
+ userCount: 4,
+ userList: expect.arrayContaining(convertObjValuesToArray(allUsers)),
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ describe('filter by unchecked email', () => {
+ it('finds only users with unchecked email', async () => {
+ await expect(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ filters: {
+ filterByActivated: false,
+ filterByDeleted: null,
+ },
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ searchUsers: {
+ userCount: 1,
+ userList: expect.arrayContaining([allUsers.garrick]),
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ describe('filter by deleted users', () => {
+ it('finds only users with deleted account', async () => {
+ await expect(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ filters: {
+ filterByActivated: null,
+ filterByDeleted: true,
+ },
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ searchUsers: {
+ userCount: 1,
+ userList: expect.arrayContaining([allUsers.stephen]),
+ },
+ },
+ }),
+ )
+ })
+ })
+
+ describe('filter by deleted account and unchecked email', () => {
+ it('finds no users', async () => {
+ await expect(
+ query({
+ query: searchUsers,
+ variables: {
+ ...variablesWithoutTextAndFilters,
+ filters: {
+ filterByActivated: false,
+ filterByDeleted: true,
+ },
+ },
+ }),
+ ).resolves.toEqual(
+ expect.objectContaining({
+ data: {
+ searchUsers: {
+ userCount: 0,
+ userList: [],
+ },
+ },
+ }),
+ )
+ })
+ })
+ })
+ })
+ })
+
+ 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)
+ })
+ })
+
+ describe('confirm two creations one after the other quickly', () => {
+ let c1: AdminPendingCreation | void
+ let c2: AdminPendingCreation | void
+
+ beforeAll(async () => {
+ const now = new Date()
+ c1 = await creationFactory(testEnv, {
+ email: 'bibi@bloxberg.de',
+ amount: 50,
+ memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
+ creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
+ })
+ c2 = await creationFactory(testEnv, {
+ email: 'bibi@bloxberg.de',
+ amount: 50,
+ memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
+ creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
+ })
+ })
+
+ // In the futrue this should not throw anymore
+ it('throws an error for the second confirmation', async () => {
+ const r1 = mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: c1 ? c1.id : -1,
+ },
+ })
+ const r2 = mutate({
+ mutation: confirmPendingCreation,
+ variables: {
+ id: c2 ? c2.id : -1,
+ },
+ })
+ await expect(r1).resolves.toEqual(
+ expect.objectContaining({
+ data: { confirmPendingCreation: true },
+ }),
+ )
+ await expect(r2).resolves.toEqual(
+ expect.objectContaining({
+ errors: [new GraphQLError('Unable to confirm creation.')],
+ }),
+ )
+ })
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index 2009af3b0..8c3d71b73 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -52,23 +52,19 @@ export class AdminResolver {
@Query(() => SearchUsersResult)
async searchUsers(
@Args()
- {
- searchText,
- currentPage = 1,
- pageSize = 25,
- notActivated = null,
- isDeleted = null,
- }: SearchUsersArgs,
+ { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
): Promise {
const userRepository = getCustomRepository(UserRepository)
const filterCriteria: ObjectLiteral[] = []
- if (notActivated !== null) {
- filterCriteria.push({ emailChecked: !notActivated })
- }
+ if (filters) {
+ if (filters.filterByActivated !== null) {
+ filterCriteria.push({ emailChecked: filters.filterByActivated })
+ }
- if (isDeleted !== null) {
- filterCriteria.push({ deletedAt: isDeleted ? Not(IsNull()) : IsNull() })
+ if (filters.filterByDeleted !== null) {
+ filterCriteria.push({ deletedAt: filters.filterByDeleted ? Not(IsNull()) : IsNull() })
+ }
}
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
@@ -157,11 +153,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,7 +166,8 @@ export class AdminResolver {
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
@Mutation(() => [Number])
async createPendingCreation(
- @Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
+ @Args() { email, amount, memo, creationDate }: CreatePendingCreationArgs,
+ @Ctx() context: Context,
): Promise {
const user = await dbUser.findOne({ email }, { withDeleted: true })
if (!user) {
@@ -181,6 +179,7 @@ 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)) {
@@ -190,7 +189,7 @@ export class AdminResolver {
adminPendingCreation.created = new Date()
adminPendingCreation.date = creationDateObj
adminPendingCreation.memo = memo
- adminPendingCreation.moderator = moderator
+ adminPendingCreation.moderator = moderator.id
await AdminPendingCreation.save(adminPendingCreation)
}
@@ -202,12 +201,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 +226,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 +237,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 +255,18 @@ export class AdminResolver {
creations = updateCreations(creations, pendingCreationToUpdate)
}
- if (!isCreationValid(creations, amount, creationDateObj)) {
- throw new Error('Creation is not valid')
- }
+ // 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 = amount
result.memo = pendingCreationToUpdate.memo
result.date = pendingCreationToUpdate.date
- result.moderator = pendingCreationToUpdate.moderator
result.creation = await getUserCreation(user.id)
@@ -298,8 +303,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 +317,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')
@@ -340,14 +351,15 @@ export class AdminResolver {
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
transaction.decay = decay ? decay.decay : new Decimal(0)
transaction.decayStart = decay ? decay.start : null
- await transaction.save()
+ await transaction.save().catch(() => {
+ throw new Error('Unable to confirm creation.')
+ })
await AdminPendingCreation.delete(pendingCreation)
@@ -426,11 +438,11 @@ export class AdminResolver {
} = {
userId,
}
- if (!filters.withRedeemed) where.redeemedBy = null
- if (!filters.withExpired) where.validUntil = MoreThan(new Date())
+ if (!filters.filterByRedeemed) where.redeemedBy = null
+ if (!filters.filterByExpired) where.validUntil = MoreThan(new Date())
const [transactionLinks, count] = await dbTransactionLink.findAndCount({
where,
- withDeleted: filters.withDeleted,
+ withDeleted: filters.filterByDeleted,
order: {
createdAt: order,
},
@@ -504,7 +516,7 @@ function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCrea
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
}
- creations[index] = creations[index].plus(pendingCreation.amount)
+ creations[index] = creations[index].plus(pendingCreation.amount.toString())
return creations
}
@@ -512,12 +524,12 @@ function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Da
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.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/BalanceResolver.ts b/backend/src/graphql/resolver/BalanceResolver.ts
index 909a22144..176b45354 100644
--- a/backend/src/graphql/resolver/BalanceResolver.ts
+++ b/backend/src/graphql/resolver/BalanceResolver.ts
@@ -1,3 +1,5 @@
+import { backendLogger as logger } from '@/server/logger'
+
import { Context, getUser } from '@/server/context'
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { Balance } from '@model/Balance'
@@ -7,7 +9,7 @@ import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { GdtResolver } from './GdtResolver'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
-import { MoreThan, getCustomRepository } from '@dbTools/typeorm'
+import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLinkRepository } from '@repository/TransactionLink'
@Resolver()
@@ -18,15 +20,22 @@ export class BalanceResolver {
const user = getUser(context)
const now = new Date()
+ logger.addContext('user', user.id)
+ logger.info(`balance(userId=${user.id})...`)
+
const gdtResolver = new GdtResolver()
const balanceGDT = await gdtResolver.gdtBalance(context)
+ logger.debug(`balanceGDT=${balanceGDT}`)
const lastTransaction = context.lastTransaction
? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { balanceDate: 'DESC' } })
+ logger.debug(`lastTransaction=${lastTransaction}`)
+
// No balance found
if (!lastTransaction) {
+ logger.info(`no balance found, return Default-Balance!`)
return new Balance({
balance: new Decimal(0),
balanceGDT,
@@ -39,16 +48,16 @@ export class BalanceResolver {
context.transactionCount || context.transactionCount === 0
? context.transactionCount
: await dbTransaction.count({ where: { userId: user.id } })
- const linkCount =
- context.linkCount || context.linkCount === 0
- ? context.linkCount
- : await dbTransactionLink.count({
- where: {
- userId: user.id,
- redeemedAt: null,
- validUntil: MoreThan(new Date()),
- },
- })
+ logger.debug(`transactionCount=${count}`)
+
+ const linkCount = await dbTransactionLink.count({
+ where: {
+ userId: user.id,
+ redeemedAt: null,
+ // validUntil: MoreThan(new Date()),
+ },
+ })
+ logger.debug(`linkCount=${linkCount}`)
// The decay is always calculated on the last booked transaction
const calculatedDecay = calculateDecay(
@@ -56,6 +65,9 @@ export class BalanceResolver {
lastTransaction.balanceDate,
now,
)
+ logger.info(
+ `calculatedDecay(balance=${lastTransaction.balance}, balanceDate=${lastTransaction.balanceDate})=${calculatedDecay}`,
+ )
// The final balance is reduced by the link amount withheld
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
@@ -63,13 +75,27 @@ export class BalanceResolver {
? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
: await transactionLinkRepository.summary(user.id, now)
- return new Balance({
- balance: calculatedDecay.balance
- .minus(sumHoldAvailableAmount.toString())
- .toDecimalPlaces(2, Decimal.ROUND_DOWN), // round towards zero
+ logger.debug(`context.sumHoldAvailableAmount=${context.sumHoldAvailableAmount}`)
+ logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
+
+ const balance = calculatedDecay.balance
+ .minus(sumHoldAvailableAmount.toString())
+ .toDecimalPlaces(2, Decimal.ROUND_DOWN) // round towards zero
+
+ // const newBalance = new Balance({
+ // balance: calculatedDecay.balance
+ // .minus(sumHoldAvailableAmount.toString())
+ // .toDecimalPlaces(2, Decimal.ROUND_DOWN),
+ const newBalance = new Balance({
+ balance,
balanceGDT,
count,
linkCount,
})
+ logger.info(
+ `new Balance(balance=${balance}, balanceGDT=${balanceGDT}, count=${count}, linkCount=${linkCount}) = ${newBalance}`,
+ )
+
+ return newBalance
}
}
diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts
index 69e1899d9..023e5b2ff 100644
--- a/backend/src/graphql/resolver/TransactionResolver.ts
+++ b/backend/src/graphql/resolver/TransactionResolver.ts
@@ -1,6 +1,7 @@
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
+import { backendLogger as logger } from '@/server/logger'
import CONFIG from '@/config'
import { Context, getUser } from '@/server/context'
@@ -44,15 +45,22 @@ export const executeTransaction = async (
recipient: dbUser,
transactionLink?: dbTransactionLink | null,
): Promise => {
+ logger.info(
+ `executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
+ )
+
if (sender.id === recipient.id) {
+ logger.error(`Sender and Recipient are the same.`)
throw new Error('Sender and Recipient are the same.')
}
if (memo.length > MEMO_MAX_CHARS) {
+ logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) {
+ logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
}
@@ -64,13 +72,16 @@ export const executeTransaction = async (
receivedCallDate,
transactionLink,
)
+ logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) {
+ logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
throw new Error("user hasn't enough GDD or amount is < 0")
}
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
await queryRunner.startTransaction('READ UNCOMMITTED')
+ logger.debug(`open Transaction to write...`)
try {
// transaction
const transactionSend = new dbTransaction()
@@ -87,6 +98,8 @@ export const executeTransaction = async (
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionSend)
+ logger.debug(`sendTransaction inserted: ${dbTransaction}`)
+
const transactionReceive = new dbTransaction()
transactionReceive.typeId = TransactionTypeId.RECEIVE
transactionReceive.memo = memo
@@ -102,12 +115,15 @@ export const executeTransaction = async (
transactionReceive.linkedTransactionId = transactionSend.id
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
await queryRunner.manager.insert(dbTransaction, transactionReceive)
+ logger.debug(`receive Transaction inserted: ${dbTransaction}`)
// Save linked transaction id for send
transactionSend.linkedTransactionId = transactionReceive.id
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
+ logger.debug(`send Transaction updated: ${transactionSend}`)
if (transactionLink) {
+ logger.info(`transactionLink: ${transactionLink}`)
transactionLink.redeemedAt = receivedCallDate
transactionLink.redeemedBy = recipient.id
await queryRunner.manager.update(
@@ -118,13 +134,15 @@ export const executeTransaction = async (
}
await queryRunner.commitTransaction()
+ logger.info(`commit Transaction successful...`)
} catch (e) {
await queryRunner.rollbackTransaction()
+ logger.error(`Transaction was not successful: ${e}`)
throw new Error(`Transaction was not successful: ${e}`)
} finally {
await queryRunner.release()
}
-
+ logger.debug(`prepare Email for transaction received...`)
// send notification email
// TODO: translate
await sendTransactionReceivedEmail({
@@ -138,7 +156,7 @@ export const executeTransaction = async (
memo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
})
-
+ logger.info(`finished executeTransaction successfully`)
return true
}
@@ -154,16 +172,21 @@ export class TransactionResolver {
const now = new Date()
const user = getUser(context)
+ logger.addContext('user', user.id)
+ logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
+
// find current balance
const lastTransaction = await dbTransaction.findOne(
{ userId: user.id },
{ order: { balanceDate: 'DESC' } },
)
+ logger.debug(`lastTransaction=${lastTransaction}`)
const balanceResolver = new BalanceResolver()
context.lastTransaction = lastTransaction
if (!lastTransaction) {
+ logger.info('no lastTransaction')
return new TransactionList(await balanceResolver.balance(context), [])
}
@@ -186,6 +209,8 @@ export class TransactionResolver {
involvedUserIds.push(transaction.linkedUserId)
}
})
+ logger.debug(`involvedUserIds=${involvedUserIds}`)
+
// We need to show the name for deleted users for old transactions
const involvedDbUsers = await dbUser
.createQueryBuilder()
@@ -193,6 +218,7 @@ export class TransactionResolver {
.where('id IN (:...userIds)', { userIds: involvedUserIds })
.getMany()
const involvedUsers = involvedDbUsers.map((u) => new User(u))
+ logger.debug(`involvedUsers=${involvedUsers}`)
const self = new User(user)
const transactions: Transaction[] = []
@@ -201,10 +227,13 @@ export class TransactionResolver {
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
await transactionLinkRepository.summary(user.id, now)
context.linkCount = transactionLinkcount
+ logger.debug(`transactionLinkcount=${transactionLinkcount}`)
context.sumHoldAvailableAmount = sumHoldAvailableAmount
+ logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
// decay & link transactions
if (currentPage === 1 && order === Order.DESC) {
+ logger.debug(`currentPage == 1: transactions=${transactions}`)
// The virtual decay is always on the booked amount, not including the generated, not yet booked links,
// since the decay is substantially different when the amount is less
transactions.push(
@@ -216,8 +245,11 @@ export class TransactionResolver {
sumHoldAvailableAmount,
),
)
+ logger.debug(`transactions=${transactions}`)
+
// virtual transaction for pending transaction-links sum
if (sumHoldAvailableAmount.greaterThan(0)) {
+ logger.debug(`sumHoldAvailableAmount > 0: transactions=${transactions}`)
transactions.push(
virtualLinkTransaction(
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
@@ -229,6 +261,7 @@ export class TransactionResolver {
self,
),
)
+ logger.debug(`transactions=${transactions}`)
}
}
@@ -240,6 +273,7 @@ export class TransactionResolver {
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
transactions.push(new Transaction(userTransaction, self, linkedUser))
})
+ logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`)
// Construct Result
return new TransactionList(await balanceResolver.balance(context), transactions)
@@ -251,29 +285,38 @@ export class TransactionResolver {
@Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: Context,
): Promise {
+ logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
+
// TODO this is subject to replay attacks
const senderUser = getUser(context)
if (senderUser.pubKey.length !== 32) {
+ logger.error(`invalid sender public key:${senderUser.pubKey}`)
throw new Error('invalid sender public key')
}
// validate recipient user
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
if (!recipientUser) {
+ logger.error(`recipient not known: email=${email}`)
throw new Error('recipient not known')
}
if (recipientUser.deletedAt) {
+ logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
throw new Error('The recipient account was deleted')
}
if (!recipientUser.emailChecked) {
+ logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
throw new Error('The recipient account is not activated')
}
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
+ logger.error(`invalid recipient public key: recipientUser=${recipientUser}`)
throw new Error('invalid recipient public key')
}
await executeTransaction(amount, memo, senderUser, recipientUser)
-
+ logger.info(
+ `successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
+ )
return true
}
}
diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts
index c658476a4..1afce832b 100644
--- a/backend/src/graphql/resolver/UserResolver.test.ts
+++ b/backend/src/graphql/resolver/UserResolver.test.ts
@@ -14,6 +14,8 @@ import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
import { printTimeDuration, activationLink } from './UserResolver'
+import { logger } from '@test/testSetup'
+
// import { klicktippSignIn } from '@/apis/KlicktippController'
jest.mock('@/mailer/sendAccountActivationEmail', () => {
@@ -43,7 +45,7 @@ let mutate: any, query: any, con: any
let testEnv: any
beforeAll(async () => {
- testEnv = await testEnvironment()
+ testEnv = await testEnvironment(logger)
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
@@ -149,12 +151,14 @@ describe('UserResolver', () => {
})
describe('email already exists', () => {
- it('throws an error', async () => {
- await expect(mutate({ mutation: createUser, variables })).resolves.toEqual(
+ it('throws and logs an error', async () => {
+ const mutation = await mutate({ mutation: createUser, variables })
+ expect(mutation).toEqual(
expect.objectContaining({
errors: [new GraphQLError('User already exists.')],
}),
)
+ expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
})
})
diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts
index 4ab5a901b..7080ad68b 100644
--- a/backend/src/graphql/resolver/UserResolver.ts
+++ b/backend/src/graphql/resolver/UserResolver.ts
@@ -1,4 +1,6 @@
import fs from 'fs'
+import { backendLogger as logger } from '@/server/logger'
+
import { Context, getUser } from '@/server/context'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository } from '@dbTools/typeorm'
@@ -43,6 +45,7 @@ const WORDS = fs
.toString()
.split(',')
const PassphraseGenerate = (): string[] => {
+ logger.trace('PassphraseGenerate...')
const result = []
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
result.push(WORDS[sodium.randombytes_random() % 2048])
@@ -51,7 +54,9 @@ const PassphraseGenerate = (): string[] => {
}
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
+ logger.trace('KeyPairEd25519Create...')
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
+ logger.error('passphrase empty or to short')
throw new Error('passphrase empty or to short')
}
@@ -79,14 +84,19 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
privKey,
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
)
+ logger.debug(`KeyPair creation ready. pubKey=${pubKey}`)
return [pubKey, privKey]
}
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
+ logger.trace('SecretKeyCryptographyCreateKey...')
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
+ logger.error(
+ `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
+ )
throw new Error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
)
@@ -115,39 +125,50 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
+ logger.debug(
+ `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`,
+ )
return [encryptionKeyHash, encryptionKey]
}
const getEmailHash = (email: string): Buffer => {
+ logger.trace('getEmailHash...')
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
sodium.crypto_generichash(emailHash, Buffer.from(email))
+ logger.debug(`getEmailHash...successful: ${emailHash}`)
return emailHash
}
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
+ logger.trace('SecretKeyCryptographyEncrypt...')
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
+ logger.debug(`SecretKeyCryptographyEncrypt...successful: ${encrypted}`)
return encrypted
}
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
+ logger.trace('SecretKeyCryptographyDecrypt...')
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
nonce.fill(31) // static nonce
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
+ logger.debug(`SecretKeyCryptographyDecrypt...successful: ${message}`)
return message
}
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
+ logger.trace('newEmailOptIn...')
const emailOptIn = new LoginEmailOptIn()
emailOptIn.verificationCode = random(64)
emailOptIn.userId = userId
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
+ logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
return emailOptIn
}
@@ -159,8 +180,14 @@ export const checkOptInCode = async (
userId: number,
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
): Promise => {
+ logger.info(`checkOptInCode... ${optInCode}`)
if (optInCode) {
if (!canResendOptIn(optInCode)) {
+ logger.error(
+ `email already sent less than ${printTimeDuration(
+ CONFIG.EMAIL_CODE_REQUEST_TIME,
+ )} minutes ago`,
+ )
throw new Error(
`email already sent less than ${printTimeDuration(
CONFIG.EMAIL_CODE_REQUEST_TIME,
@@ -170,16 +197,20 @@ export const checkOptInCode = async (
optInCode.updatedAt = new Date()
optInCode.resendCount++
} else {
+ logger.trace('create new OptIn for userId=' + userId)
optInCode = newEmailOptIn(userId)
}
optInCode.emailOptInTypeId = optInType
await LoginEmailOptIn.save(optInCode).catch(() => {
+ logger.error('Unable to save optin code= ' + optInCode)
throw new Error('Unable to save optin code.')
})
+ logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${userId}`)
return optInCode
}
export const activationLink = (optInCode: LoginEmailOptIn): string => {
+ logger.debug(`activationLink(${LoginEmailOptIn})...`)
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
}
@@ -189,6 +220,7 @@ export class UserResolver {
@Query(() => User)
@UseMiddleware(klicktippNewsletterStateMiddleware)
async verifyLogin(@Ctx() context: Context): Promise {
+ logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below)
const userEntity = getUser(context)
const user = new User(userEntity)
@@ -201,10 +233,11 @@ export class UserResolver {
const coinanimation = await userSettingRepository
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
.catch((error) => {
+ logger.error('error:', error)
throw new Error(error)
})
user.coinanimation = coinanimation
-
+ logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`)
return user
}
@@ -215,34 +248,46 @@ export class UserResolver {
@Args() { email, password, publisherId }: UnsecureLoginArgs,
@Ctx() context: Context,
): Promise {
+ logger.info(`login with ${email}, ***, ${publisherId} ...`)
email = email.trim().toLowerCase()
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
+ logger.error(`User with email=${email} does not exists`)
throw new Error('No user with this credentials')
})
if (dbUser.deletedAt) {
+ logger.error('The User was permanently deleted in database.')
throw new Error('This user was permanently deleted. Contact support for questions.')
}
if (!dbUser.emailChecked) {
+ logger.error('The Users email is not validate yet.')
throw new Error('User email not validated')
}
if (dbUser.password === BigInt(0)) {
+ logger.error('The User has not set a password yet.')
// 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 (!dbUser.pubKey || !dbUser.privKey) {
+ logger.error('The User has no private or publicKey.')
// 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(dbUser.password.toString())
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
+ logger.error('The User has no valid credentials.')
throw new Error('No user with this credentials')
}
+ // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
+ logger.addContext('user', dbUser.id)
+ logger.debug('login credentials valid...')
const user = new User(dbUser)
+ logger.debug('user=' + user)
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
+ logger.info('user.hasElopage=' + user.hasElopage)
if (!user.hasElopage && publisherId) {
user.publisherId = publisherId
dbUser.publisherId = publisherId
@@ -262,7 +307,7 @@ export class UserResolver {
key: 'token',
value: encode(dbUser.pubKey),
})
-
+ logger.info('successful Login:' + user)
return user
}
@@ -274,6 +319,9 @@ export class UserResolver {
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
// we could try to force this by sending `token: null` or `token: ''` with this call. But since it bares no real security
// we should just return true for now.
+ logger.info('Logout...')
+ // remove user.pubKey from logger-context to ensure a correct filter on log-messages belonging to the same user
+ logger.addContext('user', 'unknown')
return true
}
@@ -283,6 +331,9 @@ export class UserResolver {
@Args()
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
): Promise {
+ logger.info(
+ `createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode =${redeemCode})`,
+ )
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0;
@@ -295,7 +346,9 @@ export class UserResolver {
email = email.trim().toLowerCase()
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
+ logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
if (userFound) {
+ logger.error('User already exists with this email=' + email)
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
throw new Error(`User already exists.`)
}
@@ -314,8 +367,10 @@ export class UserResolver {
dbUser.language = language
dbUser.publisherId = publisherId
dbUser.passphrase = passphrase.join(' ')
+ logger.debug('new dbUser=' + dbUser)
if (redeemCode) {
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
+ logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
}
@@ -332,15 +387,13 @@ export class UserResolver {
await queryRunner.startTransaction('READ UNCOMMITTED')
try {
await queryRunner.manager.save(dbUser).catch((error) => {
- // eslint-disable-next-line no-console
- console.log('Error while saving dbUser', error)
+ logger.error('Error while saving dbUser', error)
throw new Error('error saving user')
})
const emailOptIn = newEmailOptIn(dbUser.id)
await queryRunner.manager.save(emailOptIn).catch((error) => {
- // eslint-disable-next-line no-console
- console.log('Error while saving emailOptIn', error)
+ logger.error('Error while saving emailOptIn', error)
throw new Error('error saving email opt in')
})
@@ -357,31 +410,35 @@ export class UserResolver {
email,
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
})
-
- /* uncomment this, when you need the activation link on the console
+ logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
+ /* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
- // eslint-disable-next-line no-console
- console.log(`Account confirmation link: ${activationLink}`)
+ logger.debug(`Account confirmation link: ${activationLink}`)
}
- */
await queryRunner.commitTransaction()
} catch (e) {
+ logger.error(`error during create user with ${e}`)
await queryRunner.rollbackTransaction()
throw e
} finally {
await queryRunner.release()
}
+ logger.info('createUser() successful...')
return new User(dbUser)
}
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
@Mutation(() => Boolean)
async forgotPassword(@Arg('email') email: string): Promise {
+ logger.info(`forgotPassword(${email})...`)
email = email.trim().toLowerCase()
const user = await DbUser.findOne({ email })
- if (!user) return true
+ if (!user) {
+ logger.warn(`no user found with ${email}`)
+ return true
+ }
// can be both types: REGISTER and RESET_PASSWORD
let optInCode = await LoginEmailOptIn.findOne({
@@ -389,7 +446,7 @@ export class UserResolver {
})
optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
-
+ logger.info(`optInCode for ${email}=${optInCode}`)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendResetPasswordEmailMailer({
link: activationLink(optInCode),
@@ -399,13 +456,12 @@ export class UserResolver {
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
})
- /* uncomment this, when you need the activation link on the console
+ /* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
- // eslint-disable-next-line no-console
- console.log(`Reset password link: ${link}`)
+ logger.debug(`Reset password link: ${activationLink(optInCode)}`)
}
- */
+ logger.info(`forgotPassword(${email}) successful...`)
return true
}
@@ -416,6 +472,7 @@ export class UserResolver {
@Arg('code') code: string,
@Arg('password') password: string,
): Promise {
+ logger.info(`setPassword(${code}, ***)...`)
// Validate Password
if (!isPassword(password)) {
throw new Error(
@@ -425,34 +482,44 @@ export class UserResolver {
// Load code
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
+ logger.error('Could not login with emailVerificationCode')
throw new Error('Could not login with emailVerificationCode')
})
-
+ logger.debug('optInCode loaded...')
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInValid(optInCode)) {
+ logger.error(
+ `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
+ )
throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
}
+ logger.debug('optInCode is valid...')
// load user
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
+ logger.error('Could not find corresponding Login User')
throw new Error('Could not find corresponding Login User')
})
+ logger.debug('user with optInCode found...')
// Generate Passphrase if needed
if (!user.passphrase) {
const passphrase = PassphraseGenerate()
user.passphrase = passphrase.join(' ')
+ logger.debug('new Passphrase generated...')
}
const passphrase = user.passphrase.split(' ')
if (passphrase.length < PHRASE_WORD_COUNT) {
+ logger.error('Could not load a correct passphrase')
// TODO if this can happen we cannot recover from that
// this seem to be good on production data, if we dont
// make a coding mistake we do not have a problem here
throw new Error('Could not load a correct passphrase')
}
+ logger.debug('Passphrase is valid...')
// Activate EMail
user.emailChecked = true
@@ -464,6 +531,7 @@ export class UserResolver {
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
user.pubKey = keyPair[0]
user.privKey = encryptedPrivkey
+ logger.debug('User credentials updated ...')
const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect()
@@ -472,12 +540,15 @@ export class UserResolver {
try {
// Save user
await queryRunner.manager.save(user).catch((error) => {
+ logger.error('error saving user: ' + error)
throw new Error('error saving user: ' + error)
})
await queryRunner.commitTransaction()
+ logger.info('User data written successfully...')
} catch (e) {
await queryRunner.rollbackTransaction()
+ logger.error('Error on writing User data:' + e)
throw e
} finally {
await queryRunner.release()
@@ -488,7 +559,11 @@ export class UserResolver {
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
try {
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
- } catch {
+ logger.debug(
+ `klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
+ )
+ } catch (e) {
+ logger.error('Error subscribe to klicktipp:' + e)
// TODO is this a problem?
// eslint-disable-next-line no-console
/* uncomment this, when you need the activation link on the console
@@ -503,13 +578,19 @@ export class UserResolver {
@Authorized([RIGHTS.QUERY_OPT_IN])
@Query(() => Boolean)
async queryOptIn(@Arg('optIn') optIn: string): Promise {
+ logger.info(`queryOptIn(${optIn})...`)
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
+ logger.debug(`found optInCode=${optInCode}`)
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
if (!isOptInValid(optInCode)) {
+ logger.error(
+ `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
+ )
throw new Error(
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
)
}
+ logger.info(`queryOptIn(${optIn}) successful...`)
return true
}
@@ -520,6 +601,9 @@ export class UserResolver {
{ firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs,
@Ctx() context: Context,
): Promise {
+ logger.info(
+ `updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***, ${coinanimation})...`,
+ )
const userEntity = getUser(context)
if (firstName) {
@@ -532,6 +616,7 @@ export class UserResolver {
if (language) {
if (!isLanguage(language)) {
+ logger.error(`"${language}" isn't a valid language`)
throw new Error(`"${language}" isn't a valid language`)
}
userEntity.language = language
@@ -540,6 +625,7 @@ export class UserResolver {
if (password && passwordNew) {
// Validate Password
if (!isPassword(passwordNew)) {
+ logger.error('newPassword does not fullfil the rules')
throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
)
@@ -548,13 +634,16 @@ export class UserResolver {
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
+ logger.error(`Old password is invalid`)
throw new Error(`Old password is invalid`)
}
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
-
+ logger.debug('oldPassword decrypted...')
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
+ logger.debug('newPasswordHash created...')
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
+ logger.debug('PrivateKey encrypted...')
// Save new password hash and newly encrypted private key
userEntity.password = newPasswordHash[0].readBigUInt64LE()
@@ -580,25 +669,30 @@ export class UserResolver {
})
await queryRunner.commitTransaction()
+ logger.debug('writing User data successful...')
} catch (e) {
await queryRunner.rollbackTransaction()
+ logger.error(`error on writing updated user data: ${e}`)
throw e
} finally {
await queryRunner.release()
}
-
+ logger.info('updateUserInfos() successfully finished...')
return true
}
@Authorized([RIGHTS.HAS_ELOPAGE])
@Query(() => Boolean)
async hasElopage(@Ctx() context: Context): Promise {
+ logger.info(`hasElopage()...`)
const userEntity = context.user
if (!userEntity) {
+ logger.info('missing context.user for EloPage-check')
return false
}
-
- return hasElopageBuys(userEntity.email)
+ const elopageBuys = hasElopageBuys(userEntity.email)
+ logger.debug(`has ElopageBuys = ${elopageBuys}`)
+ return elopageBuys
}
}
diff --git a/backend/src/mailer/sendEMail.test.ts b/backend/src/mailer/sendEMail.test.ts
index b7cc06a60..8a13c027d 100644
--- a/backend/src/mailer/sendEMail.test.ts
+++ b/backend/src/mailer/sendEMail.test.ts
@@ -2,6 +2,8 @@ import { sendEMail } from './sendEMail'
import { createTransport } from 'nodemailer'
import CONFIG from '@/config'
+import { logger } from '@test/testSetup'
+
CONFIG.EMAIL = false
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
CONFIG.EMAIL_SMTP_PORT = '1234'
@@ -26,11 +28,6 @@ jest.mock('nodemailer', () => {
describe('sendEMail', () => {
let result: boolean
describe('config email is false', () => {
- // eslint-disable-next-line no-console
- const consoleLog = console.log
- const consoleLogMock = jest.fn()
- // eslint-disable-next-line no-console
- console.log = consoleLogMock
beforeEach(async () => {
result = await sendEMail({
to: 'receiver@mail.org',
@@ -39,13 +36,8 @@ describe('sendEMail', () => {
})
})
- afterAll(() => {
- // eslint-disable-next-line no-console
- console.log = consoleLog
- })
-
- it('logs warining to console', () => {
- expect(consoleLogMock).toBeCalledWith('Emails are disabled via config')
+ it('logs warining', () => {
+ expect(logger.info).toBeCalledWith('Emails are disabled via config...')
})
it('returns false', () => {
diff --git a/backend/src/mailer/sendEMail.ts b/backend/src/mailer/sendEMail.ts
index 13c28996b..640dd7f4c 100644
--- a/backend/src/mailer/sendEMail.ts
+++ b/backend/src/mailer/sendEMail.ts
@@ -1,3 +1,4 @@
+import { backendLogger as logger } from '@/server/logger'
import { createTransport } from 'nodemailer'
import CONFIG from '@/config'
@@ -7,9 +8,10 @@ export const sendEMail = async (emailDef: {
subject: string
text: string
}): Promise => {
+ logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
+
if (!CONFIG.EMAIL) {
- // eslint-disable-next-line no-console
- console.log('Emails are disabled via config')
+ logger.info(`Emails are disabled via config...`)
return false
}
const transporter = createTransport({
@@ -27,7 +29,9 @@ export const sendEMail = async (emailDef: {
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
})
if (!info.messageId) {
+ logger.error('error sending notification email, but transaction succeed')
throw new Error('error sending notification email, but transaction succeed')
}
+ logger.info('send Email successfully.')
return true
}
diff --git a/backend/src/mailer/sendTransactionReceivedEmail.ts b/backend/src/mailer/sendTransactionReceivedEmail.ts
index 537c13d85..692f92f9a 100644
--- a/backend/src/mailer/sendTransactionReceivedEmail.ts
+++ b/backend/src/mailer/sendTransactionReceivedEmail.ts
@@ -1,3 +1,4 @@
+import { backendLogger as logger } from '@/server/logger'
import Decimal from 'decimal.js-light'
import { sendEMail } from './sendEMail'
import { transactionReceived } from './text/transactionReceived'
@@ -13,6 +14,12 @@ export const sendTransactionReceivedEmail = (data: {
memo: string
overviewURL: string
}): Promise => {
+ logger.info(
+ `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
+ <${data.email}>,
+ subject=${transactionReceived.de.subject},
+ text=${transactionReceived.de.text(data)}`,
+ )
return sendEMail({
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
subject: transactionReceived.de.subject,
diff --git a/backend/src/seeds/creation/index.ts b/backend/src/seeds/creation/index.ts
index 20d30d94c..38cb98361 100644
--- a/backend/src/seeds/creation/index.ts
+++ b/backend/src/seeds/creation/index.ts
@@ -1,6 +1,131 @@
import { CreationInterface } from './CreationInterface'
import { nMonthsBefore } from '../factory/creation'
+const bobsSendings = [
+ {
+ amount: 10,
+ memo: 'Herzlich Willkommen bei Gradido!',
+ },
+ {
+ amount: 10,
+ memo: 'für deine Hilfe, Betty',
+ },
+ {
+ amount: 23.37,
+ memo: 'für deine Hilfe, David',
+ },
+ {
+ amount: 47,
+ memo: 'für deine Hilfe, Frau Holle',
+ },
+ {
+ amount: 1.02,
+ memo: 'für deine Hilfe, Herr Müller',
+ },
+ {
+ amount: 5.67,
+ memo: 'für deine Hilfe, Maier',
+ },
+ {
+ amount: 72.93,
+ memo: 'für deine Hilfe, Elsbeth',
+ },
+ {
+ amount: 5.6,
+ memo: 'für deine Hilfe, Daniel',
+ },
+ {
+ amount: 8.87,
+ memo: 'für deine Hilfe, Yoda',
+ },
+ {
+ amount: 7.56,
+ memo: 'für deine Hilfe, Sabine',
+ },
+ {
+ amount: 7.89,
+ memo: 'für deine Hilfe, Karl',
+ },
+ {
+ amount: 8.9,
+ memo: 'für deine Hilfe, Darth Vader',
+ },
+ {
+ amount: 56.79,
+ memo: 'für deine Hilfe, Luci',
+ },
+ {
+ amount: 3.45,
+ memo: 'für deine Hilfe, Hanne',
+ },
+ {
+ amount: 8.74,
+ memo: 'für deine Hilfe, Luise',
+ },
+ {
+ amount: 7.85,
+ memo: 'für deine Hilfe, Annegred',
+ },
+ {
+ amount: 32.7,
+ memo: 'für deine Hilfe, Prinz von Zamunda',
+ },
+ {
+ amount: 44.2,
+ memo: 'für deine Hilfe, Charly Brown',
+ },
+ {
+ amount: 38.17,
+ memo: 'für deine Hilfe, Michael',
+ },
+ {
+ amount: 5.72,
+ memo: 'für deine Hilfe, Kaja',
+ },
+ {
+ amount: 3.99,
+ memo: 'für deine Hilfe, Maja',
+ },
+ {
+ amount: 4.5,
+ memo: 'für deine Hilfe, Martha',
+ },
+ {
+ amount: 8.3,
+ memo: 'für deine Hilfe, Ursula',
+ },
+ {
+ amount: 2.9,
+ memo: 'für deine Hilfe, Urs',
+ },
+ {
+ amount: 4.6,
+ memo: 'für deine Hilfe, Mecedes',
+ },
+ {
+ amount: 74.1,
+ memo: 'für deine Hilfe, Heidi',
+ },
+ {
+ amount: 4.5,
+ memo: 'für deine Hilfe, Peter',
+ },
+ {
+ amount: 5.8,
+ memo: 'für deine Hilfe, Fräulein Rottenmeier',
+ },
+]
+const bobsTransactions: CreationInterface[] = []
+bobsSendings.forEach((sending) => {
+ bobsTransactions.push({
+ email: 'bob@baumeister.de',
+ amount: sending.amount,
+ memo: sending.memo,
+ creationDate: nMonthsBefore(new Date()),
+ confirmed: true,
+ })
+})
+
export const creations: CreationInterface[] = [
{
email: 'bibi@bloxberg.de',
@@ -10,14 +135,7 @@ export const creations: CreationInterface[] = [
confirmed: true,
moveCreationDate: 12,
},
- {
- email: 'bob@baumeister.de',
- amount: 1000,
- memo: 'Herzlich Willkommen bei Gradido!',
- creationDate: nMonthsBefore(new Date()),
- confirmed: true,
- moveCreationDate: 8,
- },
+ ...bobsTransactions,
{
email: 'raeuber@hotzenplotz.de',
amount: 1000,
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 4b5913d48..d94f94b3c 100644
--- a/backend/src/seeds/factory/user.ts
+++ b/backend/src/seeds/factory/user.ts
@@ -7,7 +7,7 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
export const userFactory = async (
client: ApolloServerTestClient,
user: UserInterface,
-): Promise => {
+): Promise => {
const { mutate } = client
const {
@@ -24,13 +24,15 @@ export const userFactory = async (
})
}
- if (user.createdAt || user.deletedAt || user.isAdmin) {
- // get user from database
- const dbUser = await User.findOneOrFail({ id })
+ // get user from database
+ const dbUser = await User.findOneOrFail({ id })
+ if (user.createdAt || user.deletedAt || user.isAdmin) {
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 601b1fbbf..4598cbbe2 100644
--- a/backend/src/seeds/graphql/mutations.ts
+++ b/backend/src/seeds/graphql/mutations.ts
@@ -84,20 +84,8 @@ export const createTransactionLink = gql`
// from admin interface
export const createPendingCreation = gql`
- mutation (
- $email: String!
- $amount: Decimal!
- $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)
}
`
@@ -106,3 +94,77 @@ 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 searchUsers = gql`
+ query (
+ $searchText: String!
+ $currentPage: Int
+ $pageSize: Int
+ $filters: SearchUsersFiltersInput
+ ) {
+ searchUsers(
+ searchText: $searchText
+ currentPage: $currentPage
+ pageSize: $pageSize
+ filters: $filters
+ ) {
+ userCount
+ userList {
+ userId
+ firstName
+ lastName
+ email
+ creation
+ emailChecked
+ hasElopage
+ emailConfirmationSend
+ deletedAt
+ }
+ }
+ }
+`
+
+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 76a386953..82067c968 100644
--- a/backend/src/seeds/graphql/queries.ts
+++ b/backend/src/seeds/graphql/queries.ts
@@ -148,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..710f255ee 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: '',
@@ -26,7 +29,7 @@ const context = {
}
export const cleanDB = async () => {
- // this only works as lond we do not have foreign key constraints
+ // this only works as long we do not have foreign key constraints
for (let i = 0; i < entities.length; i++) {
await resetEntity(entities[i])
}
@@ -57,13 +60,16 @@ const run = async () => {
firstName: name.firstName(),
lastName: name.lastName(),
email: internet.email(),
- language: random.boolean() ? 'en' : 'de',
+ language: datatype.boolean() ? 'en' : 'de',
})
}
// create GDD
for (let i = 0; i < creations.length; i++) {
+ const now = new Date().getTime() // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
await creationFactory(seedClient, creations[i])
+ // eslint-disable-next-line no-empty
+ while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
}
// create Transaction Links
diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts
index 8315fda58..a0b294281 100644
--- a/backend/src/server/createServer.ts
+++ b/backend/src/server/createServer.ts
@@ -22,22 +22,32 @@ import schema from '@/graphql/schema'
import { elopageWebhook } from '@/webhook/elopage'
import { Connection } from '@dbTools/typeorm'
+import { apolloLogger } from './logger'
+import { Logger } from 'log4js'
+
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const createServer = async (context: any = serverContext): Promise => {
+const createServer = async (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ context: any = serverContext,
+ logger: Logger = apolloLogger,
+): Promise => {
+ logger.debug('createServer...')
+
// open mysql connection
const con = await connection()
if (!con || !con.isConnected) {
+ logger.fatal(`Couldn't open connection to database!`)
throw new Error(`Fatal: Couldn't open connection to database`)
}
// check for correct database version
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
if (!dbVersion) {
+ logger.fatal('Fatal: Database Version incorrect')
throw new Error('Fatal: Database Version incorrect')
}
@@ -62,8 +72,10 @@ const createServer = async (context: any = serverContext): Promise =>
introspection: CONFIG.GRAPHIQL,
context,
plugins,
+ logger,
})
apollo.applyMiddleware({ app, path: '/' })
+ logger.debug('createServer...successful')
return { apollo, app, con }
}
diff --git a/backend/src/server/logger.ts b/backend/src/server/logger.ts
new file mode 100644
index 000000000..939d7eaba
--- /dev/null
+++ b/backend/src/server/logger.ts
@@ -0,0 +1,18 @@
+import log4js from 'log4js'
+import CONFIG from '@/config'
+
+import { readFileSync } from 'fs'
+
+const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
+
+options.categories.default.level = CONFIG.LOG_LEVEL
+
+log4js.configure(options)
+
+const apolloLogger = log4js.getLogger('apollo')
+const backendLogger = log4js.getLogger('backend')
+
+apolloLogger.addContext('user', 'unknown')
+backendLogger.addContext('user', 'unknown')
+
+export { apolloLogger, backendLogger }
diff --git a/backend/src/server/plugins.ts b/backend/src/server/plugins.ts
index a407135ea..f3067d44a 100644
--- a/backend/src/server/plugins.ts
+++ b/backend/src/server/plugins.ts
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
-import { ApolloLogPlugin, LogMutateData } from 'apollo-log'
-import cloneDeep from 'lodash.clonedeep'
+import clonedeep from 'lodash.clonedeep'
const setHeadersPlugin = {
requestDidStart() {
@@ -22,24 +21,35 @@ const setHeadersPlugin = {
},
}
-const apolloLogPlugin = ApolloLogPlugin({
- mutate: (data: LogMutateData) => {
- // We need to deep clone the object in order to not modify the actual request
- const dataCopy = cloneDeep(data)
+const filterVariables = (variables: any) => {
+ const vars = clonedeep(variables)
+ if (vars.password) vars.password = '***'
+ if (vars.passwordNew) vars.passwordNew = '***'
+ return vars
+}
- // mask password if part of the query
- if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) {
- dataCopy.context.request.variables.password = '***'
+const logPlugin = {
+ requestDidStart(requestContext: any) {
+ const { logger } = requestContext
+ const { query, mutation, variables } = requestContext.request
+ logger.trace(`Request:
+${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
+ return {
+ willSendResponse(requestContext: any) {
+ if (requestContext.context.user) logger.trace(`User ID: ${requestContext.context.user.id}`)
+ if (requestContext.response.data)
+ logger.trace(`Response-Data:
+${JSON.stringify(requestContext.response.data, null, 2)}`)
+ if (requestContext.response.errors)
+ logger.trace(`Response-Errors:
+${JSON.stringify(requestContext.response.errors, null, 2)}`)
+ return requestContext
+ },
}
-
- // mask token at all times
- dataCopy.context.context.token = '***'
-
- return dataCopy
},
-})
+}
const plugins =
- process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, apolloLogPlugin]
+ process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
export default plugins
diff --git a/backend/src/typeorm/DBVersion.ts b/backend/src/typeorm/DBVersion.ts
index a8cb70489..cb53c49f1 100644
--- a/backend/src/typeorm/DBVersion.ts
+++ b/backend/src/typeorm/DBVersion.ts
@@ -1,12 +1,12 @@
import { Migration } from '@entity/Migration'
+import { backendLogger as logger } from '@/server/logger'
const getDBVersion = async (): Promise => {
try {
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
return dbVersion ? dbVersion.fileName : null
} catch (error) {
- // eslint-disable-next-line no-console
- console.log(error)
+ logger.error(error)
return null
}
}
@@ -14,8 +14,7 @@ const getDBVersion = async (): Promise => {
const checkDBVersion = async (DB_VERSION: string): Promise => {
const dbVersion = await getDBVersion()
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
- // eslint-disable-next-line no-console
- console.log(
+ logger.error(
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
dbVersion || 'None'
}`,
diff --git a/backend/src/typeorm/connection.ts b/backend/src/typeorm/connection.ts
index 745b2da94..d08d935d4 100644
--- a/backend/src/typeorm/connection.ts
+++ b/backend/src/typeorm/connection.ts
@@ -20,6 +20,9 @@ const connection = async (): Promise => {
logger: new FileLogger('all', {
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
}),
+ extra: {
+ charset: 'utf8mb4_unicode_ci',
+ },
})
} catch (error) {
// eslint-disable-next-line no-console
diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts
new file mode 100644
index 000000000..f77ad05ec
--- /dev/null
+++ b/backend/src/util/utilities.ts
@@ -0,0 +1,5 @@
+export const convertObjValuesToArray = (obj: { [x: string]: string }): Array => {
+ return Object.keys(obj).map(function (key) {
+ return obj[key]
+ })
+}
diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts
index 51610b07e..6e1856b63 100644
--- a/backend/test/helpers.ts
+++ b/backend/test/helpers.ts
@@ -25,8 +25,8 @@ export const cleanDB = async () => {
}
}
-export const testEnvironment = async () => {
- const server = await createServer(context)
+export const testEnvironment = async (logger?: any) => {
+ const server = await createServer(context, logger)
const con = server.con
const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate
diff --git a/backend/test/testSetup.ts b/backend/test/testSetup.ts
index d42836626..a43335e55 100644
--- a/backend/test/testSetup.ts
+++ b/backend/test/testSetup.ts
@@ -1,7 +1,22 @@
-/* eslint-disable no-console */
+import { backendLogger as logger } from '@/server/logger'
-// disable console.info for apollo log
-
-// eslint-disable-next-line @typescript-eslint/no-empty-function
-console.info = () => {}
jest.setTimeout(1000000)
+
+jest.mock('@/server/logger', () => {
+ const originalModule = jest.requireActual('@/server/logger')
+ return {
+ __esModule: true,
+ ...originalModule,
+ backendLogger: {
+ addContext: jest.fn(),
+ trace: jest.fn(),
+ debug: jest.fn(),
+ warn: jest.fn(),
+ info: jest.fn(),
+ error: jest.fn(),
+ fatal: jest.fn(),
+ },
+ }
+})
+
+export { logger }
diff --git a/backend/yarn.lock b/backend/yarn.lock
index f37b64d11..53a53cb9b 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -2,7 +2,7 @@
# yarn lockfile v1
-"@apollo/protobufjs@1.2.2", "@apollo/protobufjs@^1.0.3":
+"@apollo/protobufjs@1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c"
integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ==
@@ -1265,24 +1265,6 @@ apollo-link@^1.2.14:
tslib "^1.9.3"
zen-observable-ts "^0.8.21"
-apollo-log@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/apollo-log/-/apollo-log-1.1.0.tgz#e21287c917cf735b77adc06f07034f965e9b24de"
- integrity sha512-TciLu+85LSqk7t7ZGKrYN5jFiCcRMLujBjrLiOQGHGgVVkvmKlwK0oELSS9kiHQIhTq23p8qVVWb08spLpQ7Jw==
- dependencies:
- apollo-server-plugin-base "^0.10.4"
- chalk "^4.1.0"
- fast-safe-stringify "^2.0.7"
- loglevelnext "^4.0.1"
- nanoid "^3.1.20"
-
-apollo-reporting-protobuf@^0.6.2:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.2.tgz#5572866be9b77f133916532b10e15fbaa4158304"
- integrity sha512-WJTJxLM+MRHNUxt1RTl4zD0HrLdH44F2mDzMweBj1yHL0kSt8I1WwoiF/wiGVSpnG48LZrBegCaOJeuVbJTbtw==
- dependencies:
- "@apollo/protobufjs" "^1.0.3"
-
apollo-reporting-protobuf@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9"
@@ -1290,13 +1272,6 @@ apollo-reporting-protobuf@^0.8.0:
dependencies:
"@apollo/protobufjs" "1.2.2"
-apollo-server-caching@^0.5.3:
- version "0.5.3"
- resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.3.tgz#cf42a77ad09a46290a246810075eaa029b5305e1"
- integrity sha512-iMi3087iphDAI0U2iSBE9qtx9kQoMMEWr6w+LwXruBD95ek9DWyj7OeC2U/ngLjRsXM43DoBDXlu7R+uMjahrQ==
- dependencies:
- lru-cache "^6.0.0"
-
apollo-server-caching@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz#e6d1e68e3bb571cba63a61f60b434fb771c6ff39"
@@ -1335,7 +1310,7 @@ apollo-server-core@^2.25.2:
subscriptions-transport-ws "^0.9.19"
uuid "^8.0.0"
-apollo-server-env@^3.0.0, apollo-server-env@^3.1.0:
+apollo-server-env@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0"
integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==
@@ -1371,13 +1346,6 @@ apollo-server-express@^2.25.2:
subscriptions-transport-ws "^0.9.19"
type-is "^1.6.16"
-apollo-server-plugin-base@^0.10.4:
- version "0.10.4"
- resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.4.tgz#fbf73f64f95537ca9f9639dd7c535eb5eeb95dcd"
- integrity sha512-HRhbyHgHFTLP0ImubQObYhSgpmVH4Rk1BinnceZmwudIVLKrqayIVOELdyext/QnSmmzg5W7vF3NLGBcVGMqDg==
- dependencies:
- apollo-server-types "^0.6.3"
-
apollo-server-plugin-base@^0.13.0:
version "0.13.0"
resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.13.0.tgz#3f85751a420d3c4625355b6cb3fbdd2acbe71f13"
@@ -1392,15 +1360,6 @@ apollo-server-testing@^2.25.2:
dependencies:
apollo-server-core "^2.25.2"
-apollo-server-types@^0.6.3:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.6.3.tgz#f7aa25ff7157863264d01a77d7934aa6e13399e8"
- integrity sha512-aVR7SlSGGY41E1f11YYz5bvwA89uGmkVUtzMiklDhZ7IgRJhysT5Dflt5IuwDxp+NdQkIhVCErUXakopocFLAg==
- dependencies:
- apollo-reporting-protobuf "^0.6.2"
- apollo-server-caching "^0.5.3"
- apollo-server-env "^3.0.0"
-
apollo-server-types@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b"
@@ -1952,6 +1911,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-format@^4.0.9:
+ version "4.0.9"
+ resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.9.tgz#4788015ac56dedebe83b03bc361f00c1ddcf1923"
+ integrity sha512-+8J+BOUpSrlKLQLeF8xJJVTxS8QfRSuJgwxSVvslzgO3E6khbI0F5mMEPf5mTYhCCm4h99knYP6H3W9n3BQFrg==
+
debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -1973,6 +1937,13 @@ debug@^3.2.6, debug@^3.2.7:
dependencies:
ms "^2.1.1"
+debug@^4.3.4:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
+ integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
+ dependencies:
+ ms "2.1.2"
+
decimal.js-light@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
@@ -2558,11 +2529,6 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-fast-safe-stringify@^2.0.7:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884"
- integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==
-
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
@@ -2632,6 +2598,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
+flatted@^3.2.5:
+ version "3.2.5"
+ resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
+ integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
+
follow-redirects@^1.14.0:
version "1.14.4"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
@@ -2668,6 +2639,15 @@ fs-capacitor@^2.0.4:
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
+fs-extra@^10.1.0:
+ version "10.1.0"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
+ integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
+ dependencies:
+ graceful-fs "^4.2.0"
+ jsonfile "^6.0.1"
+ universalify "^2.0.0"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2818,6 +2798,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.2.4:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+graceful-fs@^4.1.6, graceful-fs@^4.2.0:
+ version "4.2.10"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
+ integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
+
graphql-extensions@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817"
@@ -3810,6 +3795,15 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
+jsonfile@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
+ integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
+ dependencies:
+ universalify "^2.0.0"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
@@ -3978,16 +3972,22 @@ lodash@4.x, lodash@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+log4js@^6.4.6:
+ version "6.4.6"
+ resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.4.6.tgz#1878aa3f09973298ecb441345fe9dd714e355c15"
+ integrity sha512-1XMtRBZszmVZqPAOOWczH+Q94AI42mtNWjvjA5RduKTSWjEc56uOBbyM1CJnfN4Ym0wSd8cQ43zOojlSHgRDAw==
+ dependencies:
+ date-format "^4.0.9"
+ debug "^4.3.4"
+ flatted "^3.2.5"
+ rfdc "^1.3.0"
+ streamroller "^3.0.8"
+
loglevel@^1.6.7:
version "1.7.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
-loglevelnext@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-4.0.1.tgz#4406c6348c243a35272ac75d7d8e4e60ecbcd011"
- integrity sha512-/tlMUn5wqgzg9msy0PiWc+8fpVXEuYPq49c2RGyw2NAh0hSrgq6j/Z3YPnwWsILMoFJ+ZT6ePHnWUonkjDnq2Q==
-
long@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
@@ -4150,11 +4150,6 @@ named-placeholders@^1.1.2:
dependencies:
lru-cache "^4.1.3"
-nanoid@^3.1.20:
- version "3.1.32"
- resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.32.tgz#8f96069e6239cc0a9ae8c0d3b41a3b4933a88c0a"
- integrity sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==
-
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -4746,6 +4741,11 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
+rfdc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
+ integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
+
rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@@ -4981,6 +4981,15 @@ stack-utils@^2.0.3:
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+streamroller@^3.0.8:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.0.8.tgz#84b190e4080ee311ca1ebe0444e30ac8eedd028d"
+ integrity sha512-VI+ni3czbFZrd1MrlybxykWZ8sMDCMtTU7YJyhgb9M5X6d1DDxLdJr+gSnmRpXPMnIWxWKMaAE8K0WumBp3lDg==
+ dependencies:
+ date-format "^4.0.9"
+ debug "^4.3.4"
+ fs-extra "^10.1.0"
+
streamsearch@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
@@ -5363,6 +5372,11 @@ universalify@^0.1.2:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
+universalify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
+ integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
+
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
diff --git a/database/.gitignore b/database/.gitignore
index bce5da58f..9e9e01ced 100644
--- a/database/.gitignore
+++ b/database/.gitignore
@@ -24,4 +24,4 @@ package-lock.json
coverage/
-*~
\ No newline at end of file
+*~
diff --git a/database/.prettierrc.js b/database/.prettierrc.js
index 8495e3f20..bc1d767d7 100644
--- a/database/.prettierrc.js
+++ b/database/.prettierrc.js
@@ -5,4 +5,5 @@ module.exports = {
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true,
+ endOfLine: "auto",
};
diff --git a/database/entity/0036-unique_previous_in_transactions/Transaction.ts b/database/entity/0036-unique_previous_in_transactions/Transaction.ts
new file mode 100644
index 000000000..99202eee4
--- /dev/null
+++ b/database/entity/0036-unique_previous_in_transactions/Transaction.ts
@@ -0,0 +1,94 @@
+import Decimal from 'decimal.js-light'
+import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
+import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
+
+@Entity('transactions')
+export class Transaction extends BaseEntity {
+ @PrimaryGeneratedColumn('increment', { unsigned: true })
+ id: number
+
+ @Column({ name: 'user_id', unsigned: true, nullable: false })
+ userId: number
+
+ @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null })
+ previous: number | null
+
+ @Column({ name: 'type_id', unsigned: true, nullable: false })
+ typeId: number
+
+ @Column({
+ type: 'decimal',
+ precision: 40,
+ scale: 20,
+ nullable: false,
+ transformer: DecimalTransformer,
+ })
+ amount: Decimal
+
+ @Column({
+ type: 'decimal',
+ precision: 40,
+ scale: 20,
+ nullable: false,
+ transformer: DecimalTransformer,
+ })
+ balance: Decimal
+
+ @Column({
+ name: 'balance_date',
+ type: 'datetime',
+ default: () => 'CURRENT_TIMESTAMP',
+ nullable: false,
+ })
+ balanceDate: Date
+
+ @Column({
+ type: 'decimal',
+ precision: 40,
+ scale: 20,
+ nullable: false,
+ transformer: DecimalTransformer,
+ })
+ decay: Decimal
+
+ @Column({
+ name: 'decay_start',
+ type: 'datetime',
+ nullable: true,
+ default: null,
+ })
+ decayStart: Date | null
+
+ @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
+ memo: string
+
+ @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null })
+ creationDate: Date | null
+
+ @Column({
+ name: 'linked_user_id',
+ type: 'int',
+ unsigned: true,
+ nullable: true,
+ default: null,
+ })
+ linkedUserId?: number | null
+
+ @Column({
+ name: 'linked_transaction_id',
+ type: 'int',
+ unsigned: true,
+ nullable: true,
+ default: null,
+ })
+ linkedTransactionId?: number | null
+
+ @Column({
+ name: 'transaction_link_id',
+ type: 'int',
+ unsigned: true,
+ nullable: true,
+ default: null,
+ })
+ transactionLinkId?: number | null
+}
diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts
index 3515202d0..5365b0f70 100644
--- a/database/entity/Transaction.ts
+++ b/database/entity/Transaction.ts
@@ -1 +1 @@
-export { Transaction } from './0032-add-transaction-link-to-transaction/Transaction'
+export { Transaction } from './0036-unique_previous_in_transactions/Transaction'
diff --git a/database/migrations/0026-combine_transaction_tables2.ts b/database/migrations/0026-combine_transaction_tables2.ts
index 3abf77354..b83c5e267 100644
--- a/database/migrations/0026-combine_transaction_tables2.ts
+++ b/database/migrations/0026-combine_transaction_tables2.ts
@@ -28,7 +28,9 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
*/
// rename `state_user_id` to `user_id`
- await queryFn('ALTER TABLE `state_user_transactions` RENAME COLUMN state_user_id TO user_id;')
+ await queryFn(
+ 'ALTER TABLE `state_user_transactions` CHANGE COLUMN state_user_id user_id int(10);',
+ )
// Create new `amount` column, with a temporary default of null
await queryFn(
'ALTER TABLE `state_user_transactions` ADD COLUMN `amount` bigint(20) DEFAULT NULL AFTER `transaction_type_id`;',
@@ -214,5 +216,7 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `memo`;')
await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `send_sender_final_balance`;')
await queryFn('ALTER TABLE `state_user_transactions` DROP COLUMN `amount`;')
- await queryFn('ALTER TABLE `state_user_transactions` RENAME COLUMN user_id TO state_user_id;')
+ await queryFn(
+ 'ALTER TABLE `state_user_transactions` CHANGE COLUMN user_id state_user_id int(10);',
+ )
}
diff --git a/database/migrations/0027-clean_transaction_table.ts b/database/migrations/0027-clean_transaction_table.ts
index b5a0e0e2e..4a427e693 100644
--- a/database/migrations/0027-clean_transaction_table.ts
+++ b/database/migrations/0027-clean_transaction_table.ts
@@ -11,23 +11,23 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) {
// drop column `transaction_id`, it is not needed
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `transaction_id`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `transaction_id`;')
// drop column `received`, it is a duplicate of balance_date
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `received`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `received`;')
// drop column `tx_hash`, it is not needed
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `tx_hash`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `tx_hash`;')
// drop column `signature`, it is not needed
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `signature`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `signature`;')
// drop column `pubkey`, it is not needed
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `pubkey`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `pubkey`;')
// drop column `creation_ident_hash`, it is not needed
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `creation_ident_hash`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `creation_ident_hash`;')
// rename `transaction_type_id` to `type_id`
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN transaction_type_id TO type_id;')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN transaction_type_id type_id int(10);')
// rename `linked_state_user_transaction_id` to `linked_transaction_id`
await queryFn(
- 'ALTER TABLE `transactions` RENAME COLUMN linked_state_user_transaction_id TO linked_transaction_id;',
+ 'ALTER TABLE `transactions` CHANGE COLUMN linked_state_user_transaction_id linked_transaction_id int(10);',
)
}
@@ -41,9 +41,9 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
// - creation_ident_hash (null)
await queryFn(
- 'ALTER TABLE `transactions` RENAME COLUMN linked_transaction_id TO linked_state_user_transaction_id;',
+ 'ALTER TABLE `transactions` CHANGE COLUMN linked_transaction_id linked_state_user_transaction_id int(10);',
)
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN type_id TO transaction_type_id;')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN type_id transaction_type_id int(10);')
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `creation_ident_hash` binary(32) DEFAULT NULL AFTER `linked_state_user_transaction_id`;',
)
diff --git a/database/migrations/0029-clean_transaction_table.ts b/database/migrations/0029-clean_transaction_table.ts
index 0b9e2cc0d..c47524b8e 100644
--- a/database/migrations/0029-clean_transaction_table.ts
+++ b/database/migrations/0029-clean_transaction_table.ts
@@ -13,35 +13,39 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
// Delete columns
// delete column `amount`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `amount`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `amount`;')
// delete column `send_sender_final_balance`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `send_sender_final_balance`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `send_sender_final_balance`;')
// delete column `balance`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `balance`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `balance`;')
// delete column `temp_dec_send_sender_final_balance`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_send_sender_final_balance`;')
+ await queryFn(
+ 'ALTER TABLE `transactions` DROP COLUMN IF EXISTS `temp_dec_send_sender_final_balance`;',
+ )
// delete column `temp_dec_diff_send_sender_final_balance`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_send_sender_final_balance`;')
+ await queryFn(
+ 'ALTER TABLE `transactions` DROP COLUMN IF EXISTS `temp_dec_diff_send_sender_final_balance`;',
+ )
// delete column `temp_dec_old_balance`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_old_balance`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `temp_dec_old_balance`;')
// delete column `temp_dec_diff_balance`
- await queryFn('ALTER TABLE `transactions` DROP COLUMN `temp_dec_diff_balance`;')
+ await queryFn('ALTER TABLE `transactions` DROP COLUMN IF EXISTS `temp_dec_diff_balance`;')
// Rename columns
// rename column `dec_amount` to `amount`
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_amount` to `amount`;')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN `dec_amount` `amount` DECIMAL(40,20);')
// rename column `dec_balance` to `balance`
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_balance` to `balance`;')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN `dec_balance` `balance` DECIMAL(40,20);')
// rename column `dec_decay` to `decay`
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN `dec_decay` to `decay`;')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN `dec_decay` `decay` DECIMAL(40,20);')
// Drop tables
// drop `state_balances`
- await queryFn('DROP TABLE `state_balances`;')
+ await queryFn('DROP TABLE IF EXISTS `state_balances`;')
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {
@@ -66,9 +70,9 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
LEFT JOIN transactions ON t.uid = transactions.user_id AND t.date = transactions.balance_date;
`)
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN `decay` to `dec_decay`;')
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN `balance` to `dec_balance`;')
- await queryFn('ALTER TABLE `transactions` RENAME COLUMN `amount` to `dec_amount`;')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN `decay` `dec_decay` DECIMAL(40,20);')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN `balance` `dec_balance` DECIMAL(40,20);')
+ await queryFn('ALTER TABLE `transactions` CHANGE COLUMN `amount` `dec_amount` DECIMAL(40,20);')
await queryFn(
'ALTER TABLE `transactions` ADD COLUMN `temp_dec_diff_balance` decimal(40,20) DEFAULT NULL AFTER linked_transaction_id;',
diff --git a/database/migrations/0035-admin_pending_creations_decimal.ts b/database/migrations/0035-admin_pending_creations_decimal.ts
index d3648f376..f76db7c97 100644
--- a/database/migrations/0035-admin_pending_creations_decimal.ts
+++ b/database/migrations/0035-admin_pending_creations_decimal.ts
@@ -8,7 +8,9 @@
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`;')
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` CHANGE COLUMN `amount` `amount_bigint` bigint(20);',
+ )
// add `amount` (decimal)
await queryFn(
'ALTER TABLE `admin_pending_creations` ADD COLUMN `amount` DECIMAL(40,20) DEFAULT NULL AFTER `amount_bigint`;',
@@ -37,6 +39,8 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom
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`;')
+ await queryFn('ALTER TABLE `admin_pending_creations` DROP COLUMN IF EXISTS `amount`;')
+ await queryFn(
+ 'ALTER TABLE `admin_pending_creations` CHANGE COLUMN `amount_bigint` `amount` bigint(20);',
+ )
}
diff --git a/database/migrations/0036-unique_previous_in_transactions.ts b/database/migrations/0036-unique_previous_in_transactions.ts
new file mode 100644
index 000000000..f05b044bb
--- /dev/null
+++ b/database/migrations/0036-unique_previous_in_transactions.ts
@@ -0,0 +1,13 @@
+/* MIGRATION TO SET previous COLUMN UNIQUE in TRANSACTION 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 `transactions` ADD UNIQUE(`previous`);')
+}
+
+export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn('ALTER TABLE `transactions` DROP INDEX `previous`;')
+}
diff --git a/database/package.json b/database/package.json
index 13c638c79..f5a16fd31 100644
--- a/database/package.json
+++ b/database/package.json
@@ -1,6 +1,6 @@
{
"name": "gradido-database",
- "version": "1.8.0",
+ "version": "1.8.3",
"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 a7e266bdf..a1751a859 100644
--- a/deployment/bare_metal/.env.dist
+++ b/deployment/bare_metal/.env.dist
@@ -38,7 +38,7 @@ KLICKTIPP_PASSWORD=
KLICKTIPP_APIKEY_DE=
KLICKTIPP_APIKEY_EN=
-EMAIL=false
+EMAIL=true
EMAIL_USERNAME=peter@lustig.de
EMAIL_SENDER=peter@lustig.de
EMAIL_PASSWORD=1234
diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md
index f39228879..f43a3d655 100644
--- a/deployment/bare_metal/setup.md
+++ b/deployment/bare_metal/setup.md
@@ -2,6 +2,9 @@
# This assums you have root access via ssh to your cleanly setup server
# Furthermore this assumes you have debian (11 64bit) running
+# Check your (Sub-)Domain with your Provider.
+# In this document gddhost.tld refers to your chosen domain
+
> ssh root@gddhost.tld
# change root default shell
@@ -87,9 +90,10 @@
# Adjust .env
# NOTE ';' can not be part of any value
+# The Github Secret is Created on Github in Settimgs -> Webhooks
> cd gradido/deployment/bare_metal
> cp .env.dist .env
> nano .env
>> Adjust values accordingly
# TODO the install.sh is not yet ready to run directly - consider to use it as pattern to do it manually
-> ./install.sh
\ No newline at end of file
+> ./install.sh
diff --git a/docu/Style/Images/Checkbox_aktiv.svg b/docu/Style/Images/Checkbox_aktiv.svg
new file mode 100644
index 000000000..3ee7c1b24
--- /dev/null
+++ b/docu/Style/Images/Checkbox_aktiv.svg
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docu/Style/Images/Checkbox_deaktiv.svg b/docu/Style/Images/Checkbox_deaktiv.svg
new file mode 100644
index 000000000..4770d4b33
--- /dev/null
+++ b/docu/Style/Images/Checkbox_deaktiv.svg
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docu/Style/Images/Footer_banner_1920x13.png b/docu/Style/Images/Footer_banner_1920x13.png
new file mode 100644
index 000000000..878c82c60
Binary files /dev/null and b/docu/Style/Images/Footer_banner_1920x13.png differ
diff --git a/docu/Style/Images/Footer_banner_396x13.png b/docu/Style/Images/Footer_banner_396x13.png
new file mode 100644
index 000000000..cd0f50750
Binary files /dev/null and b/docu/Style/Images/Footer_banner_396x13.png differ
diff --git a/docu/Style/Images/Footer_banner_768x13.png b/docu/Style/Images/Footer_banner_768x13.png
new file mode 100644
index 000000000..bea5d2f8f
Binary files /dev/null and b/docu/Style/Images/Footer_banner_768x13.png differ
diff --git a/docu/Style/Images/Footer_gradient.txt b/docu/Style/Images/Footer_gradient.txt
new file mode 100644
index 000000000..2913bb0a9
--- /dev/null
+++ b/docu/Style/Images/Footer_gradient.txt
@@ -0,0 +1,5 @@
+background: linear-gradient(90deg, rgba(197,141,56,1) 6%, rgba(243,205,124,1) 30%, rgba(219,176,86,1) 54%, rgba(238,192,95,1) 63%, rgba(204,157,61,1) 88%);
+
+height: 13px
+
+
diff --git a/frontend/.gitignore b/frontend/.gitignore
index b19667d17..0a541ba06 100644
--- a/frontend/.gitignore
+++ b/frontend/.gitignore
@@ -23,4 +23,4 @@ package-lock.json
coverage/
-*~
\ No newline at end of file
+*~
diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js
index e88113754..bc1d767d7 100644
--- a/frontend/.prettierrc.js
+++ b/frontend/.prettierrc.js
@@ -4,5 +4,6 @@ module.exports = {
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
- bracketSpacing: true
+ bracketSpacing: true,
+ endOfLine: "auto",
};
diff --git a/frontend/package.json b/frontend/package.json
index 18021e705..9d70ace58 100755
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
- "version": "1.8.0",
+ "version": "1.8.3",
"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/public/img/brand/gradido_coin●.png b/frontend/public/img/brand/gradido_coin●.png
new file mode 100644
index 000000000..15a5182da
Binary files /dev/null and b/frontend/public/img/brand/gradido_coin●.png differ
diff --git a/frontend/public/img/svg/type.svg b/frontend/public/img/svg/type.svg
new file mode 100644
index 000000000..9ab1e4c48
--- /dev/null
+++ b/frontend/public/img/svg/type.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/frontend/public/img/template/Blaetter.png b/frontend/public/img/template/Blaetter.png
new file mode 100644
index 000000000..af11b67f2
Binary files /dev/null and b/frontend/public/img/template/Blaetter.png differ
diff --git a/frontend/public/img/template/Foto_01.jpg b/frontend/public/img/template/Foto_01.jpg
new file mode 100644
index 000000000..3e64e27a5
Binary files /dev/null and b/frontend/public/img/template/Foto_01.jpg differ
diff --git a/frontend/public/img/template/gold_03.png b/frontend/public/img/template/gold_03.png
new file mode 100644
index 000000000..0704c997a
Binary files /dev/null and b/frontend/public/img/template/gold_03.png differ
diff --git a/frontend/public/img/template/gradido_background_header.png b/frontend/public/img/template/gradido_background_header.png
new file mode 100644
index 000000000..db651686f
Binary files /dev/null and b/frontend/public/img/template/gradido_background_header.png differ
diff --git a/frontend/public/img/template/logo-header.png b/frontend/public/img/template/logo-header.png
new file mode 100644
index 000000000..b35dda73d
Binary files /dev/null and b/frontend/public/img/template/logo-header.png differ
diff --git a/frontend/src/components/GddTransactionList.spec.js b/frontend/src/components/GddTransactionList.spec.js
index 37152c1c2..a6d0ef935 100644
--- a/frontend/src/components/GddTransactionList.spec.js
+++ b/frontend/src/components/GddTransactionList.spec.js
@@ -407,29 +407,34 @@ describe('GddTransactionList', () => {
})
describe('pagination buttons', () => {
+ const createTransaction = (idx) => {
+ return {
+ amount: '3.14',
+ balanceDate: '2021-04-29T17:26:40+00:00',
+ decay: {
+ decay: '-477.01',
+ start: '2021-05-13T17:46:31.000Z',
+ end: '2022-04-20T06:51:25.000Z',
+ duration: 29509494,
+ },
+ memo: 'Kreiszahl PI',
+ linkedUser: {
+ firstName: 'Bibi',
+ lastName: 'Bloxberg',
+ },
+ id: idx + 1,
+ typeId: 'RECEIVE',
+ balance: '33.33',
+ }
+ }
+
beforeEach(async () => {
+ const transactionCount = 42
await wrapper.setProps({
- transactions: Array.from({ length: 42 }, (_, idx) => {
- return {
- amount: '3.14',
- balanceDate: '2021-04-29T17:26:40+00:00',
- decay: {
- decay: '-477.01',
- start: '2021-05-13T17:46:31.000Z',
- end: '2022-04-20T06:51:25.000Z',
- duration: 29509494,
- },
- memo: 'Kreiszahl PI',
- linkedUser: {
- firstName: 'Bibi',
- lastName: 'Bloxberg',
- },
- id: idx + 1,
- typeId: 'RECEIVE',
- balance: '33.33',
- }
+ transactions: Array.from({ length: transactionCount }, (_, idx) => {
+ return createTransaction(idx)
}),
- transactionCount: 42,
+ transactionCount,
decayStartBlock,
pageSize: 25,
showPagination: true,
@@ -449,22 +454,22 @@ describe('GddTransactionList', () => {
)
})
})
- })
- describe('show no pagination', () => {
- beforeEach(async () => {
- await wrapper.setProps({
- transactions: [],
- transactionCount: 2,
- decayStartBlock,
- pageSize: 25,
- showPagination: false,
+ describe('show no pagination', () => {
+ it('shows no pagination buttons', async () => {
+ const transactionCount = 2
+ await wrapper.setProps({
+ transactions: Array.from({ length: transactionCount }, (_, idx) => {
+ return createTransaction(idx)
+ }),
+ transactionCount,
+ decayStartBlock,
+ pageSize: 25,
+ showPagination: false,
+ })
+ expect(wrapper.find('ul.pagination').exists()).toBe(false)
})
})
-
- it('shows no pagination buttons', () => {
- expect(wrapper.find('ul.pagination').exists()).toBe(false)
- })
})
})
})
diff --git a/frontend/src/components/GddTransactionList.vue b/frontend/src/components/GddTransactionList.vue
index 34f7c24ab..5becfa39e 100644
--- a/frontend/src/components/GddTransactionList.vue
+++ b/frontend/src/components/GddTransactionList.vue
@@ -61,7 +61,7 @@
[] },
pageSize: { type: Number, default: 25 },
@@ -110,6 +105,11 @@ export default {
showPagination: { type: Boolean, default: false },
pending: { type: Boolean },
},
+ data() {
+ return {
+ currentPage: 1,
+ }
+ },
methods: {
updateTransactions() {
this.$emit('update-transactions', {
@@ -123,6 +123,11 @@ export default {
return '0'
},
},
+ computed: {
+ isPaginationVisible() {
+ return this.showPagination && this.pageSize < this.transactionCount
+ },
+ },
watch: {
currentPage() {
this.updateTransactions()
@@ -134,6 +139,7 @@ export default {
},
}
+