Merge branch 'master' into 1823-Describe_Setup_on_Windows
2
.github/workflows/test.yml
vendored
@ -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 }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
6
.gitignore
vendored
@ -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
|
||||
|
||||
36
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)
|
||||
|
||||
201
LICENSE
Normal file
@ -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.
|
||||
12
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).
|
||||
|
||||
|
||||
@ -4,5 +4,6 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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),
|
||||
},
|
||||
],
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
`
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
@ -3,11 +3,23 @@
|
||||
<div class="user-search-first-div">
|
||||
<b-button class="unconfirmedRegisterMails" variant="light" @click="unconfirmedRegisterMails">
|
||||
<b-icon icon="envelope" variant="danger"></b-icon>
|
||||
{{ filterCheckedEmails ? $t('unregistered_emails') : $t('all_emails') }}
|
||||
{{
|
||||
filterByActivated === null
|
||||
? $t('all_emails')
|
||||
: filterByActivated === false
|
||||
? $t('unregistered_emails')
|
||||
: ''
|
||||
}}
|
||||
</b-button>
|
||||
<b-button class="deletedUserSearch" variant="light" @click="deletedUserSearch">
|
||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||
{{ filterDeletedUser ? $t('deleted_user') : $t('all_emails') }}
|
||||
{{
|
||||
filterByDeleted === null
|
||||
? $t('all_emails')
|
||||
: filterByDeleted === true
|
||||
? $t('deleted_user')
|
||||
: ''
|
||||
}}
|
||||
</b-button>
|
||||
</div>
|
||||
<label>{{ $t('user_search') }}</label>
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -50,3 +50,7 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
|
||||
@ -5,4 +5,5 @@ module.exports = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
|
||||
66
backend/log4js-config.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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<any> => {
|
||||
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<any> => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const apiGet = async (url: string): Promise<any> => {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
11
backend/src/graphql/arg/SearchUsersFilters.ts
Normal file
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -17,7 +17,4 @@ export default class UpdatePendingCreationArgs {
|
||||
|
||||
@Field(() => String)
|
||||
creationDate: string
|
||||
|
||||
@Field(() => Int)
|
||||
moderator: number
|
||||
}
|
||||
|
||||
@ -12,9 +12,6 @@ export class UpdatePendingCreation {
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
moderator: number
|
||||
|
||||
@Field(() => [Decimal])
|
||||
creation: Decimal[]
|
||||
}
|
||||
|
||||
1327
backend/src/graphql/resolver/AdminResolver.test.ts
Normal file
@ -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<SearchUsersResult> {
|
||||
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<Date | null> {
|
||||
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<Decimal[]> {
|
||||
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<CreatePendingCreations> {
|
||||
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<UpdatePendingCreation> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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.`,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<boolean> => {
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -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<LoginEmailOptIn> => {
|
||||
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<User> {
|
||||
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<User> {
|
||||
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<User> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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<boolean> => {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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<boolean> => {
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -17,27 +17,22 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
creation: CreationInterface,
|
||||
): Promise<void> => {
|
||||
): Promise<AdminPendingCreation | void> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
export const userFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
user: UserInterface,
|
||||
): Promise<void> => {
|
||||
): Promise<User> => {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<ServerDef> => {
|
||||
const createServer = async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: any = serverContext,
|
||||
logger: Logger = apolloLogger,
|
||||
): Promise<ServerDef> => {
|
||||
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<ServerDef> =>
|
||||
introspection: CONFIG.GRAPHIQL,
|
||||
context,
|
||||
plugins,
|
||||
logger,
|
||||
})
|
||||
apollo.applyMiddleware({ app, path: '/' })
|
||||
logger.debug('createServer...successful')
|
||||
return { apollo, app, con }
|
||||
}
|
||||
|
||||
|
||||
18
backend/src/server/logger.ts
Normal file
@ -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 }
|
||||
@ -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
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { Migration } from '@entity/Migration'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
const getDBVersion = async (): Promise<string | null> => {
|
||||
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<string | null> => {
|
||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||
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'
|
||||
}`,
|
||||
|
||||
@ -20,6 +20,9 @@ const connection = async (): Promise<Connection | null> => {
|
||||
logger: new FileLogger('all', {
|
||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
}),
|
||||
extra: {
|
||||
charset: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
5
backend/src/util/utilities.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const convertObjValuesToArray = (obj: { [x: string]: string }): Array<string> => {
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return obj[key]
|
||||
})
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -5,4 +5,5 @@ module.exports = {
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Transaction } from './0032-add-transaction-link-to-transaction/Transaction'
|
||||
export { Transaction } from './0036-unique_previous_in_transactions/Transaction'
|
||||
|
||||
@ -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);',
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,23 +11,23 @@
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// 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`;',
|
||||
)
|
||||
|
||||
@ -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<Array<any>>) {
|
||||
@ -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;',
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// 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);',
|
||||
)
|
||||
}
|
||||
|
||||
13
database/migrations/0036-unique_previous_in_transactions.ts
Normal file
@ -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<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `transactions` ADD UNIQUE(`previous`);')
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `transactions` DROP INDEX `previous`;')
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,6 +90,7 @@
|
||||
|
||||
# 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
|
||||
|
||||
33
docu/Style/Images/Checkbox_aktiv.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#047006;}
|
||||
.st1{fill:none;stroke:#F5F5F5;stroke-width:2;stroke-linecap:round;}
|
||||
.st2{display:none;}
|
||||
.st3{display:inline;}
|
||||
.st4{fill:#FFFFFF;}
|
||||
.st5{fill:none;stroke:#707070;}
|
||||
.st6{fill:none;stroke:#F5F5F5;stroke-width:2;stroke-linecap:round;stroke-opacity:0;}
|
||||
</style>
|
||||
<g id="Ebene_1">
|
||||
<g id="Gruppe_4989" transform="translate(-772 -261.959)">
|
||||
<circle id="Ellipse_17" class="st0" cx="782.16" cy="271.95" r="9.5"/>
|
||||
<line id="Linie_20" class="st1" x1="777.46" y1="271.95" x2="780.54" y2="275.47"/>
|
||||
<line id="Linie_21" class="st1" x1="780.54" y1="275.47" x2="787.53" y2="268.44"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Ebene_2" class="st2">
|
||||
<g id="Gruppe_4989_00000034808020233716670080000006186173982805041337_" transform="translate(-772 -261.959)" class="st3">
|
||||
<g id="Ellipse_17_00000100354317347125335040000001067099806688704903_" transform="translate(772 261.959)">
|
||||
<circle class="st4" cx="10.16" cy="9.99" r="9.5"/>
|
||||
<circle class="st5" cx="10.16" cy="9.99" r="9"/>
|
||||
</g>
|
||||
|
||||
<line id="Linie_20_00000062902523139473969010000009779348807157657532_" class="st6" x1="777.46" y1="271.95" x2="780.54" y2="275.47"/>
|
||||
|
||||
<line id="Linie_21_00000005969448249974932320000012316677830959789444_" class="st6" x1="780.54" y1="275.47" x2="787.53" y2="268.44"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
33
docu/Style/Images/Checkbox_deaktiv.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{display:none;}
|
||||
.st1{display:inline;}
|
||||
.st2{fill:#047006;}
|
||||
.st3{fill:none;stroke:#F5F5F5;stroke-width:2;stroke-linecap:round;}
|
||||
.st4{fill:#FFFFFF;}
|
||||
.st5{fill:none;stroke:#707070;}
|
||||
.st6{fill:none;stroke:#F5F5F5;stroke-width:2;stroke-linecap:round;stroke-opacity:0;}
|
||||
</style>
|
||||
<g id="Ebene_1" class="st0">
|
||||
<g id="Gruppe_4989" transform="translate(-772 -261.959)" class="st1">
|
||||
<circle id="Ellipse_17" class="st2" cx="782.16" cy="271.95" r="9.5"/>
|
||||
<line id="Linie_20" class="st3" x1="777.46" y1="271.95" x2="780.54" y2="275.47"/>
|
||||
<line id="Linie_21" class="st3" x1="780.54" y1="275.47" x2="787.53" y2="268.44"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Ebene_2">
|
||||
<g id="Gruppe_4989_00000034808020233716670080000006186173982805041337_" transform="translate(-772 -261.959)">
|
||||
<g id="Ellipse_17_00000100354317347125335040000001067099806688704903_" transform="translate(772 261.959)">
|
||||
<circle class="st4" cx="10.16" cy="9.99" r="9.5"/>
|
||||
<circle class="st5" cx="10.16" cy="9.99" r="9"/>
|
||||
</g>
|
||||
|
||||
<line id="Linie_20_00000062902523139473969010000009779348807157657532_" class="st6" x1="777.46" y1="271.95" x2="780.54" y2="275.47"/>
|
||||
|
||||
<line id="Linie_21_00000005969448249974932320000012316677830959789444_" class="st6" x1="780.54" y1="275.47" x2="787.53" y2="268.44"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
BIN
docu/Style/Images/Footer_banner_1920x13.png
Normal file
|
After Width: | Height: | Size: 611 B |
BIN
docu/Style/Images/Footer_banner_396x13.png
Normal file
|
After Width: | Height: | Size: 277 B |
BIN
docu/Style/Images/Footer_banner_768x13.png
Normal file
|
After Width: | Height: | Size: 405 B |
5
docu/Style/Images/Footer_gradient.txt
Normal file
@ -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
|
||||
|
||||
|
||||
@ -4,5 +4,6 @@ module.exports = {
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
BIN
frontend/public/img/brand/gradido_coin●.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
3
frontend/public/img/svg/type.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-type" viewBox="0 0 16 16">
|
||||
<path d="m2.244 13.081.943-2.803H6.66l.944 2.803H8.86L5.54 3.75H4.322L1 13.081h1.244zm2.7-7.923L6.34 9.314H3.51l1.4-4.156h.034zm9.146 7.027h.035v.896h1.128V8.125c0-1.51-1.114-2.345-2.646-2.345-1.736 0-2.59.916-2.666 2.174h1.108c.068-.718.595-1.19 1.517-1.19.971 0 1.518.52 1.518 1.464v.731H12.19c-1.647.007-2.522.8-2.522 2.058 0 1.319.957 2.18 2.345 2.18 1.06 0 1.716-.43 2.078-1.011zm-1.763.035c-.752 0-1.456-.397-1.456-1.244 0-.65.424-1.115 1.408-1.115h1.805v.834c0 .896-.752 1.525-1.757 1.525z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
BIN
frontend/public/img/template/Blaetter.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
frontend/public/img/template/Foto_01.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
frontend/public/img/template/gold_03.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
frontend/public/img/template/gradido_background_header.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/public/img/template/logo-header.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<b-pagination
|
||||
v-if="showPagination"
|
||||
v-if="isPaginationVisible"
|
||||
class="mt-3"
|
||||
pills
|
||||
size="lg"
|
||||
@ -96,11 +96,6 @@ export default {
|
||||
TransactionCreation,
|
||||
TransactionLinkSummary,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
transactions: { default: () => [] },
|
||||
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 {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
collaps-icon {
|
||||
width: 95%;
|
||||
|
||||
63
frontend/src/components/Message/Message.spec.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Message from './Message'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const propsData = {
|
||||
headline: 'site.thx.title',
|
||||
subtitle: 'site.thx.email',
|
||||
buttonText: 'login',
|
||||
linkTo: '/login',
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
describe('Message', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Message, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div.header').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('with button', () => {
|
||||
it('renders title, subtitle, and button text', () => {
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.title')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.email')
|
||||
expect(wrapper.find('.test-message-button').text()).toBe('login')
|
||||
})
|
||||
|
||||
it('button link redirects to /login', () => {
|
||||
expect(wrapper.find('a.btn').attributes('href')).toBe('/login')
|
||||
})
|
||||
})
|
||||
|
||||
describe('without button', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
buttonText: null,
|
||||
linkTo: null,
|
||||
})
|
||||
})
|
||||
|
||||
it('renders title, subtitle, and button text', () => {
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.title')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.email')
|
||||
})
|
||||
|
||||
it('button is not shown', () => {
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
36
frontend/src/components/Message/Message.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="header py-lg-6">
|
||||
<b-container class="w-50">
|
||||
<div class="header-body text-center mb-7">
|
||||
<p class="h1 test-message-headline">{{ headline }}</p>
|
||||
<p class="h4 test-message-subtitle">{{ subtitle }}</p>
|
||||
<hr />
|
||||
<b-button v-if="showButton" class="test-message-button" :to="buttonLinkTo">
|
||||
{{ buttonText }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Message',
|
||||
props: {
|
||||
headline: { type: String, required: true },
|
||||
subtitle: { type: String, required: true },
|
||||
buttonText: { type: String, required: false, default: null },
|
||||
linkTo: { type: String, required: false, default: null },
|
||||
},
|
||||
computed: {
|
||||
showButton() {
|
||||
return this.buttonText && this.linkTo
|
||||
},
|
||||
buttonLinkTo() {
|
||||
return this.linkTo ? this.linkTo : null
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -227,6 +227,36 @@ describe('TransactionLinkSummary', () => {
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
})
|
||||
|
||||
describe('close transaction link list', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper.find('div.transaction-link-details').trigger('click')
|
||||
})
|
||||
describe('reopen transaction link list', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
wrapper.find('div.transaction-link-details').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API once', () => {
|
||||
expect(apolloQueryMock).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls the API with current page one', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith({
|
||||
query: listTransactionLinks,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
})
|
||||
|
||||
it('has four transactionLinks', () => {
|
||||
expect(wrapper.vm.transactionLinks).toHaveLength(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -84,7 +84,11 @@ export default {
|
||||
this.visible = false
|
||||
} else {
|
||||
this.transactionLinks = []
|
||||
this.updateListTransactionLinks()
|
||||
if (this.currentPage === 1) {
|
||||
this.updateListTransactionLinks()
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
this.visible = true
|
||||
}
|
||||
},
|
||||
|
||||
@ -33,9 +33,12 @@
|
||||
"email-already-sent": "Wir haben dir bereits eine E-Mail vor weniger als 10 Minuten geschickt.",
|
||||
"empty-transactionlist": "Es gab einen Fehler mit der Übermittlung der Anzahl deiner Transaktionen.",
|
||||
"error": "Fehler!",
|
||||
"no-account": "Leider konnten wir keinen Account mit diesen Daten finden.",
|
||||
"no-transactionlist": "Es gab leider einen Fehler. Es wurden keine Transaktionen vom Server übermittelt",
|
||||
"session-expired": "Die Sitzung wurde aus Sicherheitsgründen beendet."
|
||||
"no-account": "Leider konnten wir keinen (aktivierten) Account mit diesen Daten finden.",
|
||||
"no-transactionlist": "Es gab leider einen Fehler. Es wurden keine Transaktionen vom Server übermittelt.",
|
||||
"no-user": "Kein Benutzer mit diesen Anmeldedaten.",
|
||||
"session-expired": "Die Sitzung wurde aus Sicherheitsgründen beendet.",
|
||||
"unknown-error": "Unbekanter Fehler: ",
|
||||
"user-already-exists": "Ein Benutzer mit diesen Daten existiert bereits."
|
||||
},
|
||||
"footer": {
|
||||
"app_version": "App version {version}",
|
||||
@ -234,7 +237,8 @@
|
||||
"register": "Du bist jetzt registriert, bitte überprüfe deine Emails und klicke auf den Aktivierungslink.",
|
||||
"reset": "Dein Passwort wurde geändert.",
|
||||
"resetPassword": "Den Code den Du genutzt hast ist zu alt bitte fordere ein neuen über die Passwort Reset Seite an.",
|
||||
"title": "Danke!"
|
||||
"title": "Danke!",
|
||||
"unsetPassword": "Dein Passwort wurde noch nicht gesetzt. Bitte setze es neu."
|
||||
}
|
||||
},
|
||||
"success": "Erfolg",
|
||||
|
||||
@ -33,9 +33,12 @@
|
||||
"email-already-sent": "We already sent you an email less than 10 minutes ago.",
|
||||
"empty-transactionlist": "There was an error with the transmission of the number of your transactions.",
|
||||
"error": "Error!",
|
||||
"no-account": "Unfortunately we could not find an account to the given data!",
|
||||
"no-account": "Unfortunately we could not find an (activated) account to the given data.",
|
||||
"no-transactionlist": "Unfortunately, there was an error. No transactions have been sent from the server.",
|
||||
"session-expired": "The session was closed for security reasons."
|
||||
"no-user": "No user with this credentials.",
|
||||
"session-expired": "The session was closed for security reasons.",
|
||||
"unknown-error": "Unknown error: ",
|
||||
"user-already-exists": "A user with this data already exists."
|
||||
},
|
||||
"footer": {
|
||||
"app_version": "App version {version}",
|
||||
@ -226,7 +229,7 @@
|
||||
"uppercase": "One uppercase letter required."
|
||||
},
|
||||
"thx": {
|
||||
"activateEmail": "Your account has not been activated yet, please check your emails and click the activation link or order a new activation link over the password reset page.",
|
||||
"activateEmail": "Your account has not been activated yet. Please check your emails and click the activation link or order a new activation link over the password reset page.",
|
||||
"checkEmail": "Your email has been successfully verified. You can sign in now.",
|
||||
"email": "We have sent you an email.",
|
||||
"emailActivated": "Thank you your email has been activated.",
|
||||
@ -234,7 +237,8 @@
|
||||
"register": "You are registered now, please check your emails and click the activation link.",
|
||||
"reset": "Your password has been changed.",
|
||||
"resetPassword": "The code you used was to old please order a new on over the password reset page.",
|
||||
"title": "Thank you!"
|
||||
"title": "Thank you!",
|
||||
"unsetPassword": "Your password has not been set yet. Please set it again."
|
||||
}
|
||||
},
|
||||
"success": "Success",
|
||||
|
||||
@ -16,6 +16,8 @@ import router from './routes/router'
|
||||
|
||||
import { apolloProvider } from './plugins/apolloProvider'
|
||||
|
||||
import 'clipboard-polyfill/overwrite-globals'
|
||||
|
||||
// plugin setup
|
||||
Vue.use(DashboardPlugin)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import ForgotPassword from './ForgotPassword'
|
||||
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
import ForgotPassword from './ForgotPassword'
|
||||
|
||||
const mockAPIcall = jest.fn()
|
||||
|
||||
@ -48,7 +47,7 @@ describe('ForgotPassword', () => {
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div.forgot-password').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div.forgot-password').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has a title', () => {
|
||||
@ -85,7 +84,7 @@ describe('ForgotPassword', () => {
|
||||
})
|
||||
|
||||
it('has a submit button', () => {
|
||||
expect(form.find('button[type="submit"]').exists()).toBeTruthy()
|
||||
expect(form.find('button[type="submit"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('invalid Email', () => {
|
||||
@ -118,19 +117,25 @@ describe('ForgotPassword', () => {
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('toasts a standard error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.email-already-sent')
|
||||
it('shows error title, subtitle, login button', () => {
|
||||
expect(wrapper.vm.showPageMessage).toBe(true)
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.errorTitle')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('error.email-already-sent')
|
||||
expect(wrapper.find('.test-message-button').text()).toBe('login')
|
||||
})
|
||||
|
||||
it('pushes to "/thx/forgotPassword"', () => {
|
||||
expect(mockAPIcall).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
email: 'user@example.org',
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/thx/forgotPassword')
|
||||
it('button link directs to "/login"', () => {
|
||||
expect(wrapper.find('.test-message-button').attributes('href')).toBe('/login')
|
||||
})
|
||||
|
||||
it.skip('click redirects to "/login"', async () => {
|
||||
// wrapper.find('.test-message-button').trigger('click')
|
||||
// await wrapper.vm.$nextTick()
|
||||
expect(mockRouterPush).toBeCalledWith('/login')
|
||||
})
|
||||
|
||||
it('toasts a standard error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.email-already-sent')
|
||||
})
|
||||
})
|
||||
|
||||
@ -147,15 +152,19 @@ describe('ForgotPassword', () => {
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('pushes to "/thx/forgotPassword"', () => {
|
||||
expect(mockAPIcall).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
email: 'user@example.org',
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/thx/forgotPassword')
|
||||
it('shows success title, subtitle, login button', () => {
|
||||
expect(wrapper.vm.showPageMessage).toBe(true)
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.title')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.email')
|
||||
expect(wrapper.find('.test-message-button').text()).toBe('login')
|
||||
})
|
||||
|
||||
it('button link redirects to "/login"', () => {
|
||||
expect(wrapper.find('.test-message-button').attributes('href')).toBe('/login')
|
||||
})
|
||||
|
||||
it.skip('click redirects to "/login"', () => {
|
||||
// expect(mockRouterPush).toBeCalledWith('/login')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
<b-container class="mt--8 p-1">
|
||||
<b-container v-if="enterData" class="mt--8 p-1">
|
||||
<b-row class="justify-content-center">
|
||||
<b-col lg="6" md="8">
|
||||
<b-card no-body class="border-0 gradido-custom-background">
|
||||
@ -36,24 +36,48 @@
|
||||
<router-link to="/login" class="mt-3">{{ $t('back') }}</router-link>
|
||||
</div>
|
||||
</b-container>
|
||||
<b-container v-else class="mt--8 p-1">
|
||||
<message
|
||||
v-if="success"
|
||||
:headline="$t('site.thx.title')"
|
||||
:subtitle="$t('site.thx.email')"
|
||||
:buttonText="$t('login')"
|
||||
linkTo="/login"
|
||||
/>
|
||||
<message
|
||||
v-else
|
||||
:headline="$t('site.thx.errorTitle')"
|
||||
:subtitle="$t('error.email-already-sent')"
|
||||
:buttonText="$t('login')"
|
||||
linkTo="/login"
|
||||
/>
|
||||
</b-container>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { forgotPassword } from '@/graphql/mutations'
|
||||
import InputEmail from '@/components/Inputs/InputEmail'
|
||||
import Message from '@/components/Message/Message'
|
||||
|
||||
export default {
|
||||
name: 'ForgotPassword',
|
||||
components: {
|
||||
InputEmail,
|
||||
Message,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
disable: 'disabled',
|
||||
form: {
|
||||
email: '',
|
||||
},
|
||||
subtitle: 'settings.password.subtitle',
|
||||
showPageMessage: false,
|
||||
success: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.$route.params.comingFrom) {
|
||||
this.subtitle = 'settings.password.resend_subtitle'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -66,18 +90,20 @@ export default {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push('/thx/forgotPassword')
|
||||
this.showPageMessage = true
|
||||
this.success = true
|
||||
})
|
||||
.catch(() => {
|
||||
this.showPageMessage = true
|
||||
this.success = false
|
||||
this.toastError(this.$t('error.email-already-sent'))
|
||||
this.$router.push('/thx/forgotPassword')
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.$route.params.comingFrom) {
|
||||
this.subtitle = 'settings.password.resend_subtitle'
|
||||
}
|
||||
computed: {
|
||||
enterData() {
|
||||
return !this.showPageMessage
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { RouterLinkStub, mount } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import Login from './Login'
|
||||
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
import Login from './Login'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -60,7 +59,7 @@ describe('Login', () => {
|
||||
})
|
||||
|
||||
it('renders the Login form', () => {
|
||||
expect(wrapper.find('div.login-form').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div.login-form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('Login header', () => {
|
||||
@ -103,19 +102,19 @@ describe('Login', () => {
|
||||
|
||||
describe('Login form', () => {
|
||||
it('has a login form', () => {
|
||||
expect(wrapper.find('form').exists()).toBeTruthy()
|
||||
expect(wrapper.find('form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an Email input field', () => {
|
||||
expect(wrapper.find('input[placeholder="Email"]').exists()).toBeTruthy()
|
||||
expect(wrapper.find('input[placeholder="Email"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an Password input field', () => {
|
||||
expect(wrapper.find('input[placeholder="form.password"]').exists()).toBeTruthy()
|
||||
expect(wrapper.find('input[placeholder="form.password"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has a Submit button', () => {
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBeTruthy()
|
||||
expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -205,61 +204,111 @@ describe('Login', () => {
|
||||
})
|
||||
|
||||
describe('login fails', () => {
|
||||
beforeEach(async () => {
|
||||
const createError = async (errorMessage) => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: errorMessage,
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: '..No user with this credentials',
|
||||
})
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('hides the spinner', () => {
|
||||
expect(spinnerHideMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.no-account')
|
||||
})
|
||||
}
|
||||
|
||||
describe('login fails with "User email not validated"', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: 'User email not validated',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
await createError('GraphQL error: User email not validated.')
|
||||
})
|
||||
|
||||
it('redirects to /thx/login', () => {
|
||||
expect(mockRouterPush).toBeCalledWith('/thx/login')
|
||||
it('hides the spinner', () => {
|
||||
expect(spinnerHideMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('shows error title, subtitle, login button', () => {
|
||||
expect(wrapper.vm.showPageMessage).toBe(true)
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.errorTitle')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.activateEmail')
|
||||
expect(wrapper.find('.test-message-button').text()).toBe('settings.password.reset')
|
||||
})
|
||||
|
||||
it('button link directs to "/forgot-password"', () => {
|
||||
expect(wrapper.find('.test-message-button').attributes('href')).toBe('/forgot-password')
|
||||
})
|
||||
|
||||
it.skip('click redirects to "/forgot-password"', async () => {
|
||||
// wrapper.find('.test-message-button').trigger('click')
|
||||
// await flushPromises()
|
||||
// await wrapper.vm.$nextTick()
|
||||
// expect(mockRouterPush).toBeCalledWith('/forgot-password')
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.no-account')
|
||||
})
|
||||
})
|
||||
|
||||
describe('login fails with "User has no password set yet"', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
message: 'User has no password set yet',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('input[placeholder="Email"]').setValue('user@example.org')
|
||||
await wrapper.find('input[placeholder="form.password"]').setValue('1234')
|
||||
await flushPromises()
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
await createError('GraphQL error: User has no password set yet.')
|
||||
})
|
||||
|
||||
it('redirects to /reset-password/login', () => {
|
||||
expect(mockRouterPush).toBeCalledWith('/reset-password/login')
|
||||
it('shows error title, subtitle, login button', () => {
|
||||
expect(wrapper.vm.showPageMessage).toBe(true)
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.errorTitle')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.unsetPassword')
|
||||
expect(wrapper.find('.test-message-button').text()).toBe('settings.password.reset')
|
||||
})
|
||||
|
||||
it('button link directs to "/reset-password/login"', () => {
|
||||
expect(wrapper.find('.test-message-button').attributes('href')).toBe(
|
||||
'/reset-password/login',
|
||||
)
|
||||
})
|
||||
|
||||
it.skip('click redirects to "/reset-password/login"', () => {
|
||||
// expect(mockRouterPush).toBeCalledWith('/reset-password/login')
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.no-account')
|
||||
})
|
||||
})
|
||||
|
||||
describe('login fails with "No user with this credentials"', () => {
|
||||
beforeEach(async () => {
|
||||
await createError('GraphQL error: No user with this credentials.')
|
||||
})
|
||||
|
||||
it('shows no error message on the page', () => {
|
||||
// don't show any error on the page! against boots
|
||||
expect(wrapper.vm.showPageMessage).toBe(false)
|
||||
expect(wrapper.find('.test-message-headline').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-subtitle').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.no-user')
|
||||
})
|
||||
})
|
||||
|
||||
describe('login fails with an unknow error', () => {
|
||||
beforeEach(async () => {
|
||||
await createError(' – Unknow error')
|
||||
})
|
||||
|
||||
it('shows no error message on the page', () => {
|
||||
// don't show any error on the page! against boots
|
||||
expect(wrapper.vm.showPageMessage).toBe(false)
|
||||
expect(wrapper.find('.test-message-headline').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-subtitle').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.unknown-error – Unknow error')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
<b-container class="mt--8">
|
||||
<b-container v-if="enterData" class="mt--8 p-1">
|
||||
<b-row class="justify-content-center">
|
||||
<b-col lg="5" md="7">
|
||||
<b-card no-body class="border-0 mb-0 gradido-custom-background">
|
||||
@ -57,11 +57,21 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<b-container v-else class="mt--8 p-1">
|
||||
<message
|
||||
:headline="$t('site.thx.errorTitle')"
|
||||
:subtitle="errorSubtitle"
|
||||
:buttonText="$t('settings.password.reset')"
|
||||
:linkTo="errorLinkTo"
|
||||
/>
|
||||
</b-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InputPassword from '@/components/Inputs/InputPassword'
|
||||
import InputEmail from '@/components/Inputs/InputEmail'
|
||||
import Message from '@/components/Message/Message'
|
||||
import { login } from '@/graphql/queries'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
@ -70,6 +80,7 @@ export default {
|
||||
components: {
|
||||
InputPassword,
|
||||
InputEmail,
|
||||
Message,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -78,6 +89,10 @@ export default {
|
||||
password: '',
|
||||
},
|
||||
passwordVisible: false,
|
||||
showPageMessage: false,
|
||||
errorReason: null,
|
||||
errorSubtitle: '',
|
||||
errorLinkTo: '',
|
||||
CONFIG,
|
||||
}
|
||||
},
|
||||
@ -109,15 +124,31 @@ export default {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(this.$t('error.no-account'))
|
||||
if (error.message.includes('User email not validated')) {
|
||||
this.$router.push('/thx/login')
|
||||
this.showPageMessage = true
|
||||
this.errorSubtitle = this.$t('site.thx.activateEmail')
|
||||
this.errorLinkTo = '/forgot-password'
|
||||
this.toastError(this.$t('error.no-account'))
|
||||
} else if (error.message.includes('User has no password set yet')) {
|
||||
this.$router.push('/reset-password/login')
|
||||
this.showPageMessage = true
|
||||
this.errorSubtitle = this.$t('site.thx.unsetPassword')
|
||||
this.errorLinkTo = '/reset-password/login'
|
||||
this.toastError(this.$t('error.no-account'))
|
||||
} else if (error.message.includes('No user with this credentials')) {
|
||||
// don't show any error on the page! against boots
|
||||
this.toastError(this.$t('error.no-user'))
|
||||
} else {
|
||||
// don't show any error on the page! against boots
|
||||
this.toastError(this.$t('error.unknown-error') + error.message)
|
||||
}
|
||||
loader.hide()
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
enterData() {
|
||||
return !this.showPageMessage
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { toastErrorSpy } from '@test/testSetup'
|
||||
import Register from './Register'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -49,7 +50,7 @@ describe('Register', () => {
|
||||
})
|
||||
|
||||
it('renders the Register form', () => {
|
||||
expect(wrapper.find('div#registerform').exists()).toBeTruthy()
|
||||
expect(wrapper.find('div#registerform').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('Register header', () => {
|
||||
@ -93,22 +94,22 @@ describe('Register', () => {
|
||||
|
||||
describe('Register form', () => {
|
||||
it('has a register form', () => {
|
||||
expect(wrapper.find('form').exists()).toBeTruthy()
|
||||
expect(wrapper.find('form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has firstname input fields', () => {
|
||||
expect(wrapper.find('#registerFirstname').exists()).toBeTruthy()
|
||||
expect(wrapper.find('#registerFirstname').exists()).toBe(true)
|
||||
})
|
||||
it('has lastname input fields', () => {
|
||||
expect(wrapper.find('#registerLastname').exists()).toBeTruthy()
|
||||
expect(wrapper.find('#registerLastname').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has email input fields', () => {
|
||||
expect(wrapper.find('#Email-input-field').exists()).toBeTruthy()
|
||||
expect(wrapper.find('#Email-input-field').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has Language selected field', () => {
|
||||
expect(wrapper.find('.selectedLanguage').exists()).toBeTruthy()
|
||||
expect(wrapper.find('.selectedLanguage').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('selects Language value en', async () => {
|
||||
@ -117,7 +118,7 @@ describe('Register', () => {
|
||||
})
|
||||
|
||||
it('has 1 checkbox input fields', () => {
|
||||
expect(wrapper.find('#registerCheckbox').exists()).toBeTruthy()
|
||||
expect(wrapper.find('#registerCheckbox').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has PublisherId input fields', () => {
|
||||
@ -220,42 +221,63 @@ describe('Register', () => {
|
||||
})
|
||||
|
||||
describe('server sends back error', () => {
|
||||
beforeEach(async () => {
|
||||
registerUserMutationMock.mockRejectedValue({ message: 'Ouch!' })
|
||||
const createError = async (errorMessage) => {
|
||||
registerUserMutationMock.mockRejectedValue({
|
||||
message: errorMessage,
|
||||
})
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
describe('server sends back error "User already exists."', () => {
|
||||
beforeEach(async () => {
|
||||
await createError('GraphQL error: User already exists.')
|
||||
})
|
||||
|
||||
it('shows no error message on the page', () => {
|
||||
// don't show any error on the page! against boots
|
||||
expect(wrapper.vm.showPageMessage).toBe(false)
|
||||
expect(wrapper.find('.test-message-headline').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-subtitle').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.user-already-exists')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows error message', () => {
|
||||
expect(wrapper.find('span.alert-text').exists()).toBeTruthy()
|
||||
expect(wrapper.find('span.alert-text').text().length !== 0).toBeTruthy()
|
||||
expect(wrapper.find('span.alert-text').text()).toContain('error.error')
|
||||
expect(wrapper.find('span.alert-text').text()).toContain('Ouch!')
|
||||
})
|
||||
describe('server sends back error "Unknown error"', () => {
|
||||
beforeEach(async () => {
|
||||
await createError(' – Unknown error.')
|
||||
})
|
||||
|
||||
it('button to dismisses error message is present', () => {
|
||||
expect(wrapper.find('button.close').exists()).toBeTruthy()
|
||||
})
|
||||
it('shows no error message on the page', () => {
|
||||
// don't show any error on the page! against boots
|
||||
expect(wrapper.vm.showPageMessage).toBe(false)
|
||||
expect(wrapper.find('.test-message-headline').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-subtitle').exists()).toBe(false)
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('dismisses error message', async () => {
|
||||
await wrapper.find('button.close').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.alert-text').exists()).not.toBeTruthy()
|
||||
it('toasts the error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('error.unknown-error – Unknown error.')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('server sends back success', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
registerUserMutationMock.mockResolvedValue({
|
||||
data: {
|
||||
create: 'success',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('routes to "/thx/register"', async () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
|
||||
it('submit sends apollo mutate', () => {
|
||||
expect(registerUserMutationMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
@ -267,7 +289,16 @@ describe('Register', () => {
|
||||
},
|
||||
}),
|
||||
)
|
||||
expect(routerPushMock).toHaveBeenCalledWith('/thx/register')
|
||||
})
|
||||
|
||||
it('shows success title, subtitle', () => {
|
||||
expect(wrapper.vm.showPageMessage).toBe(true)
|
||||
expect(wrapper.find('.test-message-headline').text()).toBe('site.thx.title')
|
||||
expect(wrapper.find('.test-message-subtitle').text()).toBe('site.thx.register')
|
||||
})
|
||||
|
||||
it('button is not present', () => {
|
||||
expect(wrapper.find('.test-message-button').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Page content -->
|
||||
<b-container class="mt--8 p-1">
|
||||
<b-container v-if="enterData" class="mt--8 p-1">
|
||||
<!-- Table -->
|
||||
|
||||
<b-row class="justify-content-center">
|
||||
@ -105,19 +105,6 @@
|
||||
</b-form-checkbox>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-alert
|
||||
v-if="showError"
|
||||
show
|
||||
dismissible
|
||||
variant="danger"
|
||||
@dismissed="closeAlert"
|
||||
>
|
||||
<span class="alert-icon"><i class="ni ni-point"></i></span>
|
||||
<span class="alert-text">
|
||||
<strong>{{ $t('error.error') }}</strong>
|
||||
{{ messageError }}
|
||||
</span>
|
||||
</b-alert>
|
||||
|
||||
<b-row v-b-toggle:my-collapse class="text-muted shadow-sm p-3 publisherCollaps">
|
||||
<b-col>{{ $t('publisher.publisherId') }} {{ $store.state.publisherId }}</b-col>
|
||||
@ -177,6 +164,9 @@
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<b-container v-else class="mt--8 p-1">
|
||||
<message :headline="$t('site.thx.title')" :subtitle="$t('site.thx.register')" />
|
||||
</b-container>
|
||||
<!--
|
||||
<div class="text-center pt-4">
|
||||
<router-link class="test-button-another-community" to="/select-community">
|
||||
@ -189,13 +179,18 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import InputEmail from '@/components/Inputs/InputEmail.vue'
|
||||
import LanguageSwitchSelect from '@/components/LanguageSwitchSelect.vue'
|
||||
import { createUser } from '@/graphql/mutations'
|
||||
import CONFIG from '@/config'
|
||||
import InputEmail from '@/components/Inputs/InputEmail.vue'
|
||||
import LanguageSwitchSelect from '@/components/LanguageSwitchSelect.vue'
|
||||
import Message from '@/components/Message/Message'
|
||||
|
||||
export default {
|
||||
components: { InputEmail, LanguageSwitchSelect },
|
||||
components: {
|
||||
InputEmail,
|
||||
LanguageSwitchSelect,
|
||||
Message,
|
||||
},
|
||||
name: 'Register',
|
||||
data() {
|
||||
return {
|
||||
@ -206,10 +201,8 @@ export default {
|
||||
agree: false,
|
||||
},
|
||||
language: '',
|
||||
showPageMessage: false,
|
||||
submitted: false,
|
||||
showError: false,
|
||||
messageError: '',
|
||||
register: true,
|
||||
publisherId: this.$store.state.publisherId,
|
||||
redeemCode: this.$route.params.code,
|
||||
CONFIG,
|
||||
@ -240,20 +233,22 @@ export default {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$router.push('/thx/register')
|
||||
this.showPageMessage = true
|
||||
})
|
||||
.catch((error) => {
|
||||
this.showError = true
|
||||
this.messageError = error.message
|
||||
// don't show any error on the page! against boots
|
||||
let errorMessage
|
||||
switch (error.message) {
|
||||
case 'GraphQL error: User already exists.':
|
||||
errorMessage = this.$t('error.user-already-exists')
|
||||
break
|
||||
default:
|
||||
errorMessage = this.$t('error.unknown-error') + error.message
|
||||
break
|
||||
}
|
||||
this.toastError(errorMessage)
|
||||
})
|
||||
},
|
||||
closeAlert() {
|
||||
this.showError = false
|
||||
this.messageError = ''
|
||||
this.form.email = ''
|
||||
this.form.firstname = ''
|
||||
this.form.lastname = ''
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
namesFilled() {
|
||||
@ -270,6 +265,9 @@ export default {
|
||||
disabled() {
|
||||
return !(this.namesFilled && this.emailFilled && this.form.agree && !!this.language)
|
||||
},
|
||||
enterData() {
|
||||
return !this.showPageMessage
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
:transactionCount="transactionCount"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
:transactions="transactions"
|
||||
:show-pagination="true"
|
||||
:showPagination="true"
|
||||
@update-transactions="updateTransactions"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
|
||||
@ -4856,6 +4856,11 @@ cli-width@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
|
||||
integrity sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==
|
||||
|
||||
clipboard-polyfill@^4.0.0-rc1:
|
||||
version "4.0.0-rc1"
|
||||
resolved "https://registry.yarnpkg.com/clipboard-polyfill/-/clipboard-polyfill-4.0.0-rc1.tgz#a000ab25b1f582bca03667dc572854f1c8d92b04"
|
||||
integrity sha512-Cel03Es9ZgP6pYA2JT9cZ2VgvOH2/EHgB7jji84FpINBJWqfMEwiI1Y3LstVL+E43cm3CnCrLL2vwb9DMbr28A==
|
||||
|
||||
clipboardy@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-2.3.0.tgz#3c2903650c68e46a91b388985bc2774287dba290"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.3",
|
||||
"description": "Gradido",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:gradido/gradido.git",
|
||||
|
||||