mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 1574-Concept_to_introduce_Gradido-ID
This commit is contained in:
commit
6a5f49f485
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -528,7 +528,7 @@ jobs:
|
|||||||
report_name: Coverage Backend
|
report_name: Coverage Backend
|
||||||
type: lcov
|
type: lcov
|
||||||
result_path: ./backend/coverage/lcov.info
|
result_path: ./backend/coverage/lcov.info
|
||||||
min_coverage: 54
|
min_coverage: 66
|
||||||
token: ${{ github.token }}
|
token: ${{ github.token }}
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
|
.dbeaver
|
||||||
|
.project
|
||||||
*.log
|
*.log
|
||||||
/node_modules/*
|
/node_modules/*
|
||||||
messages.pot
|
messages.pot
|
||||||
@ -11,3 +13,9 @@ package-lock.json
|
|||||||
/deployment/bare_metal/nginx/update-page/updating.html
|
/deployment/bare_metal/nginx/update-page/updating.html
|
||||||
/deployment/bare_metal/log
|
/deployment/bare_metal/log
|
||||||
/deployment/bare_metal/backup
|
/deployment/bare_metal/backup
|
||||||
|
|
||||||
|
# Node Version Manager configuration file
|
||||||
|
.nvmrc
|
||||||
|
|
||||||
|
# Apple macOS folder attribute file
|
||||||
|
.DS_Store
|
||||||
|
|||||||
176
CHANGELOG.md
176
CHANGELOG.md
@ -4,8 +4,184 @@ 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).
|
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)
|
||||||
|
- Fix: config value for the redeem URL was missing [`#1828`](https://github.com/gradido/gradido/pull/1828)
|
||||||
|
- Refactor: Database admin pending creations use decimal [`#1748`](https://github.com/gradido/gradido/pull/1748)
|
||||||
|
- refactor: Drop Server User Table [`#1808`](https://github.com/gradido/gradido/pull/1808)
|
||||||
|
- 1816 expired link are not highlighted [`#1821`](https://github.com/gradido/gradido/pull/1821)
|
||||||
|
- 1812 put qr code into popup on generate [`#1820`](https://github.com/gradido/gradido/pull/1820)
|
||||||
|
- Docu: Federation image [`#1817`](https://github.com/gradido/gradido/pull/1817)
|
||||||
|
- 1813 qr code popup [`#1819`](https://github.com/gradido/gradido/pull/1819)
|
||||||
|
- Fix: cross-env for windows [`#1822`](https://github.com/gradido/gradido/pull/1822)
|
||||||
|
- fix: Double Load Transaction Links [`#1818`](https://github.com/gradido/gradido/pull/1818)
|
||||||
|
- Generated link in backend should also give back the base url [`#1745`](https://github.com/gradido/gradido/pull/1745)
|
||||||
|
- 1731 style startDecayStartblock, style Adapted across pages [`#1809`](https://github.com/gradido/gradido/pull/1809)
|
||||||
|
- Refactor: Frontend bake in community info [`#1750`](https://github.com/gradido/gradido/pull/1750)
|
||||||
|
- fix: Load Transaction Link Details on Click [`#1806`](https://github.com/gradido/gradido/pull/1806)
|
||||||
|
- devops: Deploy Seed in Backend [`#1790`](https://github.com/gradido/gradido/pull/1790)
|
||||||
|
- refactor: Balance Model and Decay Rounding [`#1780`](https://github.com/gradido/gradido/pull/1780)
|
||||||
|
- change config DECAY_START_TIME in UTC 0000 [`#1807`](https://github.com/gradido/gradido/pull/1807)
|
||||||
|
- 1751 make gdt visible only if explicitly clicked [`#1752`](https://github.com/gradido/gradido/pull/1752)
|
||||||
|
- add Tab system from bootstrap in SearchUserTable Userdata [`#1744`](https://github.com/gradido/gradido/pull/1744)
|
||||||
|
- Fix: Certbot renewal [`#1789`](https://github.com/gradido/gradido/pull/1789)
|
||||||
|
- 🍰 Add Wallet Link To Mails [`#1765`](https://github.com/gradido/gradido/pull/1765)
|
||||||
|
- 1633 display qr code on link in transaction list [`#1661`](https://github.com/gradido/gradido/pull/1661)
|
||||||
|
- 1755 insert additional text when redeeming [`#1756`](https://github.com/gradido/gradido/pull/1756)
|
||||||
|
- refactor: Define Context Interface [`#1762`](https://github.com/gradido/gradido/pull/1762)
|
||||||
|
- fix: Elopage Status [`#1742`](https://github.com/gradido/gradido/pull/1742)
|
||||||
|
- Refactor: Frontend decay start block as static config value [`#1749`](https://github.com/gradido/gradido/pull/1749)
|
||||||
|
- better date format for reddem valid date [`#1758`](https://github.com/gradido/gradido/pull/1758)
|
||||||
|
- add insert shadow in summary links transaction type [`#1754`](https://github.com/gradido/gradido/pull/1754)
|
||||||
|
- Feature: JWT duration is now 30min by default [`#1747`](https://github.com/gradido/gradido/pull/1747)
|
||||||
|
- Docu: Scope of Gradido [`#1746`](https://github.com/gradido/gradido/pull/1746)
|
||||||
|
- fix: Check That Recipient User Has Activated Account to Receive Coins [`#1743`](https://github.com/gradido/gradido/pull/1743)
|
||||||
|
- Fix: Fixed config dist version to properly reflect new password reset url [`#1737`](https://github.com/gradido/gradido/pull/1737)
|
||||||
|
- 503 transaction list pagination pages clickable [`#1677`](https://github.com/gradido/gradido/pull/1677)
|
||||||
|
- if no recipientEmail else form.email [`#1722`](https://github.com/gradido/gradido/pull/1722)
|
||||||
|
- 1727 change button text and observe spelling [`#1728`](https://github.com/gradido/gradido/pull/1728)
|
||||||
|
- 1729 load spinner if pending balance [`#1730`](https://github.com/gradido/gradido/pull/1730)
|
||||||
|
- transaction type remains when jumping from the verification back [`#1724`](https://github.com/gradido/gradido/pull/1724)
|
||||||
|
- text for toast expand link copied [`#1726`](https://github.com/gradido/gradido/pull/1726)
|
||||||
|
|
||||||
|
#### [1.7.1](https://github.com/gradido/gradido/compare/1.7.0...1.7.1)
|
||||||
|
|
||||||
|
> 1 April 2022
|
||||||
|
|
||||||
|
- v1.7.1 [`#1721`](https://github.com/gradido/gradido/pull/1721)
|
||||||
|
- fix: Localize Dates on Redeem Transaction Link Page [`#1720`](https://github.com/gradido/gradido/pull/1720)
|
||||||
|
- fix: Round Virtual Transaction Link Transaction [`#1718`](https://github.com/gradido/gradido/pull/1718)
|
||||||
|
- larger icon and deacy information if center [`#1719`](https://github.com/gradido/gradido/pull/1719)
|
||||||
|
- Fix: restore script load correct .env [`#1717`](https://github.com/gradido/gradido/pull/1717)
|
||||||
|
- fix-disbled-button-if-totalBalance [`#1716`](https://github.com/gradido/gradido/pull/1716)
|
||||||
|
- icon droplet-halflarger and correctly positioned [`#1713`](https://github.com/gradido/gradido/pull/1713)
|
||||||
|
- fix: Clean up Registration Flow [`#1709`](https://github.com/gradido/gradido/pull/1709)
|
||||||
|
- 1703 submit button disabled when total amount to submit is minus [`#1705`](https://github.com/gradido/gradido/pull/1705)
|
||||||
|
- add extra disabled variable for send emit, disabled send by emit [`#1704`](https://github.com/gradido/gradido/pull/1704)
|
||||||
|
- Fix: Correct calculation of decay [`#1699`](https://github.com/gradido/gradido/pull/1699)
|
||||||
|
- Fix: Allow sending of more then half of my wealth via link [`#1700`](https://github.com/gradido/gradido/pull/1700)
|
||||||
|
- feat: Seed Creations Months Ago From Now [`#1702`](https://github.com/gradido/gradido/pull/1702)
|
||||||
|
- Fix: Frontend show proper error message on failed send [`#1701`](https://github.com/gradido/gradido/pull/1701)
|
||||||
|
|
||||||
|
#### [1.7.0](https://github.com/gradido/gradido/compare/1.6.6...1.7.0)
|
||||||
|
|
||||||
|
> 30 March 2022
|
||||||
|
|
||||||
|
- v1.7.0 [`#1698`](https://github.com/gradido/gradido/pull/1698)
|
||||||
|
- folder for new style images [`#1694`](https://github.com/gradido/gradido/pull/1694)
|
||||||
|
- fix: No Email Exposed on Forgot Password [`#1696`](https://github.com/gradido/gradido/pull/1696)
|
||||||
|
- fix: No Decay Calculation in Frontend [`#1692`](https://github.com/gradido/gradido/pull/1692)
|
||||||
|
- fix: Wrong Balance on Decay Transaction [`#1691`](https://github.com/gradido/gradido/pull/1691)
|
||||||
|
- fix: No Plus Before Zero Decay [`#1689`](https://github.com/gradido/gradido/pull/1689)
|
||||||
|
- fix: Update Deployment env.dist [`#1688`](https://github.com/gradido/gradido/pull/1688)
|
||||||
|
- 1684 when generating a link form does not reset [`#1687`](https://github.com/gradido/gradido/pull/1687)
|
||||||
|
- Refactor: Multicreation - do not show unactivated emails [`#1679`](https://github.com/gradido/gradido/pull/1679)
|
||||||
|
- feat: Show Link Duration in Emails [`#1663`](https://github.com/gradido/gradido/pull/1663)
|
||||||
|
- refactor: Balance Resolver [`#1665`](https://github.com/gradido/gradido/pull/1665)
|
||||||
|
- refactor: Set Email Optin Valid Time to 24 hours [`#1662`](https://github.com/gradido/gradido/pull/1662)
|
||||||
|
- Fix: Fixes found on Stage1 [`#1683`](https://github.com/gradido/gradido/pull/1683)
|
||||||
|
- 1555 admin see user generated link [`#1656`](https://github.com/gradido/gradido/pull/1656)
|
||||||
|
- 1594 show transaction was created by link [`#1680`](https://github.com/gradido/gradido/pull/1680)
|
||||||
|
- refactor: Memo Text Length to 255 Characters [`#1675`](https://github.com/gradido/gradido/pull/1675)
|
||||||
|
- adminarea: fetchPolicy on searchUser deleted User [`#1678`](https://github.com/gradido/gradido/pull/1678)
|
||||||
|
- 1223 community communication concept [`#1313`](https://github.com/gradido/gradido/pull/1313)
|
||||||
|
- clear form.email if click send per link, tests if clicked [`#1660`](https://github.com/gradido/gradido/pull/1660)
|
||||||
|
- feat: User in Transaction Clickable to Send Directly [`#1658`](https://github.com/gradido/gradido/pull/1658)
|
||||||
|
- feat: Add Sender Email to Transaction Received Mail [`#1664`](https://github.com/gradido/gradido/pull/1664)
|
||||||
|
- Feature: Enforce config versions [`#1627`](https://github.com/gradido/gradido/pull/1627)
|
||||||
|
- 1559 frontend transport redeem link through register [`#1647`](https://github.com/gradido/gradido/pull/1647)
|
||||||
|
- update-balance if link succesfully generated [`#1655`](https://github.com/gradido/gradido/pull/1655)
|
||||||
|
- feat: Add Referrer ID to Users [`#1654`](https://github.com/gradido/gradido/pull/1654)
|
||||||
|
- 1558 - show tranaction link information page [`#1625`](https://github.com/gradido/gradido/pull/1625)
|
||||||
|
- refactor: No Float Ids [`#1624`](https://github.com/gradido/gradido/pull/1624)
|
||||||
|
- Change the text if the account is not activated yet and changed the b… [`#1336`](https://github.com/gradido/gradido/pull/1336)
|
||||||
|
- Refactor: Corrected name of transaction link summary [`#1628`](https://github.com/gradido/gradido/pull/1628)
|
||||||
|
- fix: Query for Only Creations Transaction List [`#1623`](https://github.com/gradido/gradido/pull/1623)
|
||||||
|
- Fix: build for development and production links external modules properly [`#1626`](https://github.com/gradido/gradido/pull/1626)
|
||||||
|
- feat: Seed Transaction Links [`#1622`](https://github.com/gradido/gradido/pull/1622)
|
||||||
|
- 1588 frontend expendable paginated link list [`#1620`](https://github.com/gradido/gradido/pull/1620)
|
||||||
|
- feat: Seed Creation Transactions in Backend [`#1621`](https://github.com/gradido/gradido/pull/1621)
|
||||||
|
- Feature: Eslint i18n validation [`#1618`](https://github.com/gradido/gradido/pull/1618)
|
||||||
|
- refactor: Seed in Backend [`#1619`](https://github.com/gradido/gradido/pull/1619)
|
||||||
|
- 1554 frontend transaction link summary [`#1613`](https://github.com/gradido/gradido/pull/1613)
|
||||||
|
- Frontend generate link for send gdd [`#1579`](https://github.com/gradido/gradido/pull/1579)
|
||||||
|
- feat: Test Logout in User Resolver [`#1617`](https://github.com/gradido/gradido/pull/1617)
|
||||||
|
- Fix: Logrotate & Log Dates & Save Update Log & Correct tag Checkout [`#1612`](https://github.com/gradido/gradido/pull/1612)
|
||||||
|
- refactor: No Reset DB in Backend Unit Tests [`#1616`](https://github.com/gradido/gradido/pull/1616)
|
||||||
|
- Test: Require 53% backend coverage [`#1611`](https://github.com/gradido/gradido/pull/1611)
|
||||||
|
- 1599 components for transactionlist types [`#1600`](https://github.com/gradido/gradido/pull/1600)
|
||||||
|
- feat: Link Transaction to Transaction Link on Redeem [`#1610`](https://github.com/gradido/gradido/pull/1610)
|
||||||
|
- feat: Redeem Transaction Link Mutation [`#1607`](https://github.com/gradido/gradido/pull/1607)
|
||||||
|
- feat: List Transaction Links Query [`#1606`](https://github.com/gradido/gradido/pull/1606)
|
||||||
|
- feat: Virtual Transaction for Transaction Links [`#1603`](https://github.com/gradido/gradido/pull/1603)
|
||||||
|
- refactor: Transaction Link Query [`#1605`](https://github.com/gradido/gradido/pull/1605)
|
||||||
|
- 1216 seo vorschau links [`#1426`](https://github.com/gradido/gradido/pull/1426)
|
||||||
|
- Feature: Eslint style rules & Stylelint for SCSS [`#1598`](https://github.com/gradido/gradido/pull/1598)
|
||||||
|
- refactor: Remove showEmail from Transaction Links [`#1602`](https://github.com/gradido/gradido/pull/1602)
|
||||||
|
- feat: Delete Transaction Link Mutation [`#1597`](https://github.com/gradido/gradido/pull/1597)
|
||||||
|
- Query-transaction-link [`#1586`](https://github.com/gradido/gradido/pull/1586)
|
||||||
|
- feat: Create Transaction Link Mutation [`#1585`](https://github.com/gradido/gradido/pull/1585)
|
||||||
|
- feat: Model Transaction Link [`#1584`](https://github.com/gradido/gradido/pull/1584)
|
||||||
|
- feat: Test Login in User Resolver [`#1538`](https://github.com/gradido/gradido/pull/1538)
|
||||||
|
- add style in App.vue, set class .pointer on transaction-list-item [`#1583`](https://github.com/gradido/gradido/pull/1583)
|
||||||
|
- feat: Use Vue Filter to Display Gradido Amounts [`#1576`](https://github.com/gradido/gradido/pull/1576)
|
||||||
|
- refactor: Resolve Relative Paths in Backend [`#1572`](https://github.com/gradido/gradido/pull/1572)
|
||||||
|
- refactor: Frontend Directory Structure and Routes [`#1571`](https://github.com/gradido/gradido/pull/1571)
|
||||||
|
- community name in creation transaction is displayed cleanly [`#1578`](https://github.com/gradido/gradido/pull/1578)
|
||||||
|
- Planning: send new users gradido [`#1567`](https://github.com/gradido/gradido/pull/1567)
|
||||||
|
- Refactor arithmetic merge [`#1548`](https://github.com/gradido/gradido/pull/1548)
|
||||||
|
- Adminarea creation transactionlist show [`#1550`](https://github.com/gradido/gradido/pull/1550)
|
||||||
|
- Fix: Validate password on UpdateUserInfos [`#1568`](https://github.com/gradido/gradido/pull/1568)
|
||||||
|
- Fix: Allow sending to user without transactions [`#1549`](https://github.com/gradido/gradido/pull/1549)
|
||||||
|
- Fix: Balance type [`#1569`](https://github.com/gradido/gradido/pull/1569)
|
||||||
|
- Refactor: arithmetic and data types [`#1539`](https://github.com/gradido/gradido/pull/1539)
|
||||||
|
- refactor: Bootstrap Vue Toast in Admin interface [`#1547`](https://github.com/gradido/gradido/pull/1547)
|
||||||
|
- Refactor: Combine transaction tables and restructure transaction design [`#1531`](https://github.com/gradido/gradido/pull/1531)
|
||||||
|
|
||||||
#### [1.6.6](https://github.com/gradido/gradido/compare/1.6.5...1.6.6)
|
#### [1.6.6](https://github.com/gradido/gradido/compare/1.6.5...1.6.6)
|
||||||
|
|
||||||
|
> 28 February 2022
|
||||||
|
|
||||||
|
- v1.6.6 [`#1541`](https://github.com/gradido/gradido/pull/1541)
|
||||||
- Fix: Upper case email on register breaks account [`#1542`](https://github.com/gradido/gradido/pull/1542)
|
- Fix: Upper case email on register breaks account [`#1542`](https://github.com/gradido/gradido/pull/1542)
|
||||||
- 1106 first transaction cannot be expanded [`#1432`](https://github.com/gradido/gradido/pull/1432)
|
- 1106 first transaction cannot be expanded [`#1432`](https://github.com/gradido/gradido/pull/1432)
|
||||||
- added missing bootstrap scss. bootstrap/scss/bootstrap, plus more mis… [`#1540`](https://github.com/gradido/gradido/pull/1540)
|
- added missing bootstrap scss. bootstrap/scss/bootstrap, plus more mis… [`#1540`](https://github.com/gradido/gradido/pull/1540)
|
||||||
|
|||||||
201
LICENSE
Normal file
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
12
README.md
@ -93,3 +93,15 @@ Note: The Changelog will be regenerated with all tags on release on the external
|
|||||||
## Useful Links
|
## Useful Links
|
||||||
|
|
||||||
- [Gradido.net](https://gradido.net/)
|
- [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,
|
singleQuote: true,
|
||||||
trailingComma: "all",
|
trailingComma: "all",
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
bracketSpacing: true
|
bracketSpacing: true,
|
||||||
|
endOfLine: "auto",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Administraion Interface for Gradido",
|
"description": "Administraion Interface for Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Moriz Wahl",
|
"author": "Moriz Wahl",
|
||||||
"version": "1.6.6",
|
"version": "1.8.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -14,7 +14,7 @@
|
|||||||
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json",
|
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json",
|
||||||
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
|
||||||
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
|
||||||
"test": "TZ=UTC jest --coverage",
|
"test": "cross-env TZ=UTC jest --coverage",
|
||||||
"locales": "scripts/sort.sh"
|
"locales": "scripts/sort.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -57,6 +57,7 @@
|
|||||||
"@vue/cli-service": "~4.5.0",
|
"@vue/cli-service": "~4.5.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-plugin-transform-require-context": "^0.1.1",
|
"babel-plugin-transform-require-context": "^0.1.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "7.25.0",
|
"eslint": "7.25.0",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.3.0",
|
||||||
"eslint-config-standard": "^16.0.3",
|
"eslint-config-standard": "^16.0.3",
|
||||||
|
|||||||
@ -24,12 +24,6 @@ const mocks = {
|
|||||||
},
|
},
|
||||||
$store: {
|
$store: {
|
||||||
commit: stateCommitMock,
|
commit: stateCommitMock,
|
||||||
state: {
|
|
||||||
moderator: {
|
|
||||||
id: 0,
|
|
||||||
name: 'test moderator',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +116,6 @@ describe('CreationFormular', () => {
|
|||||||
creationDate: getCreationDate(2),
|
creationDate: getCreationDate(2),
|
||||||
amount: 90,
|
amount: 90,
|
||||||
memo: 'Test create coins',
|
memo: 'Test create coins',
|
||||||
moderator: 0,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -370,14 +363,12 @@ describe('CreationFormular', () => {
|
|||||||
creationDate: getCreationDate(1),
|
creationDate: getCreationDate(1),
|
||||||
amount: 200,
|
amount: 200,
|
||||||
memo: 'Test mass create coins',
|
memo: 'Test mass create coins',
|
||||||
moderator: 0,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
creationDate: getCreationDate(1),
|
creationDate: getCreationDate(1),
|
||||||
amount: 200,
|
amount: 200,
|
||||||
memo: 'Test mass create coins',
|
memo: 'Test mass create coins',
|
||||||
moderator: 0,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -154,7 +154,6 @@ export default {
|
|||||||
creationDate: this.selected.date,
|
creationDate: this.selected.date,
|
||||||
amount: Number(this.value),
|
amount: Number(this.value),
|
||||||
memo: this.text,
|
memo: this.text,
|
||||||
moderator: Number(this.$store.state.moderator.id),
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.$apollo
|
this.$apollo
|
||||||
@ -188,7 +187,6 @@ export default {
|
|||||||
creationDate: this.selected.date,
|
creationDate: this.selected.date,
|
||||||
amount: Number(this.value),
|
amount: Number(this.value),
|
||||||
memo: this.text,
|
memo: this.text,
|
||||||
moderator: Number(this.$store.state.moderator.id),
|
|
||||||
}
|
}
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
|
import CreationTransactionList from './CreationTransactionList.vue'
|
||||||
import { toastErrorSpy } from '../../test/testSetup'
|
import { toastErrorSpy } from '../../test/testSetup'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
@ -46,11 +46,11 @@ const propsData = {
|
|||||||
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('CreationTransactionListFormular', () => {
|
describe('CreationTransactionList', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(CreationTransactionListFormular, { localVue, mocks, propsData })
|
return mount(CreationTransactionList, { localVue, mocks, propsData })
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="component-creation-transaction-list">
|
<div class="component-creation-transaction-list">
|
||||||
{{ $t('transactionlist.title') }}
|
<div class="h3">{{ $t('transactionlist.title') }}</div>
|
||||||
<b-table striped hover :fields="fields" :items="items"></b-table>
|
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -11,7 +11,6 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
|
|||||||
amount: 500,
|
amount: 500,
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
memo: 'Test Schöpfung 2',
|
memo: 'Test Schöpfung 2',
|
||||||
moderator: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -28,12 +27,6 @@ const mocks = {
|
|||||||
mutate: apolloMutateMock,
|
mutate: apolloMutateMock,
|
||||||
},
|
},
|
||||||
$store: {
|
$store: {
|
||||||
state: {
|
|
||||||
moderator: {
|
|
||||||
id: 0,
|
|
||||||
name: 'test moderator',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
commit: stateCommitMock,
|
commit: stateCommitMock,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -104,7 +97,6 @@ describe('EditCreationFormular', () => {
|
|||||||
creationDate: getCreationDate(0),
|
creationDate: getCreationDate(0),
|
||||||
amount: 500,
|
amount: 500,
|
||||||
memo: 'Test Schöpfung 2',
|
memo: 'Test Schöpfung 2',
|
||||||
moderator: 0,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -129,7 +121,6 @@ describe('EditCreationFormular', () => {
|
|||||||
amount: 500,
|
amount: 500,
|
||||||
date: expect.any(Date),
|
date: expect.any(Date),
|
||||||
memo: 'Test Schöpfung 2',
|
memo: 'Test Schöpfung 2',
|
||||||
moderator: 0,
|
|
||||||
row: expect.any(Object),
|
row: expect.any(Object),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -120,7 +120,6 @@ export default {
|
|||||||
creationDate: this.selected.date,
|
creationDate: this.selected.date,
|
||||||
amount: Number(this.value),
|
amount: Number(this.value),
|
||||||
memo: this.text,
|
memo: this.text,
|
||||||
moderator: Number(this.$store.state.moderator.id),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@ -129,7 +128,6 @@ export default {
|
|||||||
amount: Number(result.data.updatePendingCreation.amount),
|
amount: Number(result.data.updatePendingCreation.amount),
|
||||||
date: result.data.updatePendingCreation.date,
|
date: result.data.updatePendingCreation.date,
|
||||||
memo: result.data.updatePendingCreation.memo,
|
memo: result.data.updatePendingCreation.memo,
|
||||||
moderator: Number(result.data.updatePendingCreation.moderator),
|
|
||||||
row: this.row,
|
row: this.row,
|
||||||
})
|
})
|
||||||
this.toastSuccess(
|
this.toastSuccess(
|
||||||
|
|||||||
129
admin/src/components/Tables/OpenCreationsTable.spec.js
Normal file
129
admin/src/components/Tables/OpenCreationsTable.spec.js
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import OpenCreationsTable from './OpenCreationsTable.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||||
|
const apolloQueryMock = jest.fn().mockResolvedValue({})
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
firstName: 'Bob',
|
||||||
|
lastName: 'der Baumeister',
|
||||||
|
email: 'bob@baumeister.de',
|
||||||
|
amount: 300,
|
||||||
|
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||||
|
date: '2022-01-01T00:00:00.000Z',
|
||||||
|
moderator: 1,
|
||||||
|
creation: [700, 1000, 1000],
|
||||||
|
__typename: 'PendingCreation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
firstName: 'Räuber',
|
||||||
|
lastName: 'Hotzenplotz',
|
||||||
|
email: 'raeuber@hotzenplotz.de',
|
||||||
|
amount: 210,
|
||||||
|
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||||
|
date: '2022-01-01T00:00:00.000Z',
|
||||||
|
moderator: 1,
|
||||||
|
creation: [790, 1000, 1000],
|
||||||
|
__typename: 'PendingCreation',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
firstName: 'Stephen',
|
||||||
|
lastName: 'Hawking',
|
||||||
|
email: 'stephen@hawking.uk',
|
||||||
|
amount: 330,
|
||||||
|
memo: 'Aktives Grundeinkommen für Januar 2022',
|
||||||
|
date: '2022-01-01T00:00:00.000Z',
|
||||||
|
moderator: 1,
|
||||||
|
creation: [670, 1000, 1000],
|
||||||
|
__typename: 'PendingCreation',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fields: [
|
||||||
|
{ key: 'bookmark', label: 'delete' },
|
||||||
|
{ key: 'email', label: 'e_mail' },
|
||||||
|
{ key: 'firstName', label: 'firstname' },
|
||||||
|
{ key: 'lastName', label: 'lastname' },
|
||||||
|
{
|
||||||
|
key: 'amount',
|
||||||
|
label: 'creation',
|
||||||
|
formatter: (value) => {
|
||||||
|
return value + ' GDD'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'memo', label: 'text' },
|
||||||
|
{
|
||||||
|
key: 'date',
|
||||||
|
label: 'date',
|
||||||
|
formatter: (value) => {
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'moderator', label: 'moderator' },
|
||||||
|
{ key: 'edit_creation', label: 'edit' },
|
||||||
|
{ key: 'confirm', label: 'save' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$d: jest.fn((d) => d),
|
||||||
|
$apollo: {
|
||||||
|
mutate: apolloMutateMock,
|
||||||
|
query: apolloQueryMock,
|
||||||
|
},
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
moderator: {
|
||||||
|
id: 0,
|
||||||
|
name: 'test moderator',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('OpenCreationsTable', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(OpenCreationsTable, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV element with the class .open-creations-table', () => {
|
||||||
|
expect(wrapper.find('div.open-creations-table').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a table with three rows', () => {
|
||||||
|
expect(wrapper.findAll('tbody > tr')).toHaveLength(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('find first button.bi-pencil-square for open EditCreationFormular ', () => {
|
||||||
|
expect(wrapper.findAll('tr').at(1).find('.bi-pencil-square').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('show edit details', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await wrapper.findAll('tr').at(1).find('.bi-pencil-square').trigger('click')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('has a component element with name EditCreationFormular', () => {
|
||||||
|
expect(wrapper.findComponent({ name: 'EditCreationFormular' }).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('renders the component component-edit-creation-formular', () => {
|
||||||
|
expect(wrapper.find('div.component-edit-creation-formular').exists()).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="component-open-creations-table">
|
<div class="open-creations-table">
|
||||||
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
||||||
<template #cell(bookmark)="row">
|
<template #cell(bookmark)="row">
|
||||||
<b-button
|
<b-button
|
||||||
|
|||||||
@ -49,6 +49,8 @@
|
|||||||
|
|
||||||
<template #row-details="row">
|
<template #row-details="row">
|
||||||
<b-card ref="rowDetails" class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
|
<b-card ref="rowDetails" class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
|
||||||
|
<b-tabs content-class="mt-3">
|
||||||
|
<b-tab :title="$t('creation')" active :disabled="row.item.deletedAt !== null">
|
||||||
<creation-formular
|
<creation-formular
|
||||||
v-if="!row.item.deletedAt"
|
v-if="!row.item.deletedAt"
|
||||||
type="singleCreation"
|
type="singleCreation"
|
||||||
@ -58,7 +60,8 @@
|
|||||||
:creationUserData="creationUserData"
|
:creationUserData="creationUserData"
|
||||||
@update-user-data="updateUserData"
|
@update-user-data="updateUserData"
|
||||||
/>
|
/>
|
||||||
<div v-else>{{ $t('userIsDeleted') }}</div>
|
</b-tab>
|
||||||
|
<b-tab :title="$t('e_mail')" :disabled="row.item.deletedAt !== null">
|
||||||
<confirm-register-mail-formular
|
<confirm-register-mail-formular
|
||||||
v-if="!row.item.deletedAt"
|
v-if="!row.item.deletedAt"
|
||||||
:checked="row.item.emailChecked"
|
:checked="row.item.emailChecked"
|
||||||
@ -69,11 +72,17 @@
|
|||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<creation-transaction-list-formular
|
</b-tab>
|
||||||
v-if="!row.item.deletedAt"
|
<b-tab :title="$t('creationList')" :disabled="row.item.deletedAt !== null">
|
||||||
:userId="row.item.userId"
|
<creation-transaction-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
|
||||||
/>
|
</b-tab>
|
||||||
|
<b-tab :title="$t('transactionlink.name')" :disabled="row.item.deletedAt !== null">
|
||||||
|
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
|
||||||
|
</b-tab>
|
||||||
|
<b-tab :title="$t('delete_user')">
|
||||||
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
||||||
|
</b-tab>
|
||||||
|
</b-tabs>
|
||||||
</b-card>
|
</b-card>
|
||||||
</template>
|
</template>
|
||||||
</b-table>
|
</b-table>
|
||||||
@ -82,7 +91,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import CreationFormular from '../CreationFormular.vue'
|
import CreationFormular from '../CreationFormular.vue'
|
||||||
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
||||||
import CreationTransactionListFormular from '../CreationTransactionListFormular.vue'
|
import CreationTransactionList from '../CreationTransactionList.vue'
|
||||||
|
import TransactionLinkList from '../TransactionLinkList.vue'
|
||||||
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -90,7 +100,8 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
CreationFormular,
|
CreationFormular,
|
||||||
ConfirmRegisterMailFormular,
|
ConfirmRegisterMailFormular,
|
||||||
CreationTransactionListFormular,
|
CreationTransactionList,
|
||||||
|
TransactionLinkList,
|
||||||
DeletedUserFormular,
|
DeletedUserFormular,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
140
admin/src/components/TransactionLinkList.spec.js
Normal file
140
admin/src/components/TransactionLinkList.spec.js
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import TransactionLinkList from './TransactionLinkList.vue'
|
||||||
|
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
|
||||||
|
import { toastErrorSpy } from '../../test/testSetup'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const apolloQueryMock = jest.fn()
|
||||||
|
apolloQueryMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
listTransactionLinksAdmin: {
|
||||||
|
linkCount: 8,
|
||||||
|
linkList: [
|
||||||
|
{
|
||||||
|
amount: '19.99',
|
||||||
|
code: '62ef8236ace7217fbd066c5a',
|
||||||
|
createdAt: '2022-03-24T17:43:09.000Z',
|
||||||
|
deletedAt: null,
|
||||||
|
holdAvailableAmount: '20.51411720068412022949',
|
||||||
|
id: 36,
|
||||||
|
memo: 'Kein Trick, keine Zauberrei,\nbei Gradidio sei dabei!',
|
||||||
|
redeemedAt: null,
|
||||||
|
validUntil: '2022-04-07T17:43:09.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
amount: '19.99',
|
||||||
|
code: '2b603f36521c617fbd066cef',
|
||||||
|
createdAt: '2022-03-24T17:43:09.000Z',
|
||||||
|
deletedAt: null,
|
||||||
|
holdAvailableAmount: '20.51411720068412022949',
|
||||||
|
id: 37,
|
||||||
|
memo: 'Kein Trick, keine Zauberrei,\nbei Gradidio sei dabei!',
|
||||||
|
redeemedAt: null,
|
||||||
|
validUntil: '2022-04-07T17:43:09.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
amount: '19.99',
|
||||||
|
code: '0bb789b5bd5b717fbd066eb5',
|
||||||
|
createdAt: '2022-03-24T17:43:09.000Z',
|
||||||
|
deletedAt: '2022-03-24T17:43:09.000Z',
|
||||||
|
holdAvailableAmount: '20.51411720068412022949',
|
||||||
|
id: 40,
|
||||||
|
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||||
|
redeemedAt: '2022-04-07T14:43:09.000Z',
|
||||||
|
validUntil: '2022-04-07T17:43:09.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
amount: '19.99',
|
||||||
|
code: '2d4a763e516b317fbd066a85',
|
||||||
|
createdAt: '2022-01-01T00:00:00.000Z',
|
||||||
|
deletedAt: null,
|
||||||
|
holdAvailableAmount: '20.51411720068412022949',
|
||||||
|
id: 33,
|
||||||
|
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||||
|
redeemedAt: null,
|
||||||
|
validUntil: '2022-01-15T00:00:00.000Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
userId: 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$apollo: {
|
||||||
|
query: apolloQueryMock,
|
||||||
|
},
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$d: jest.fn((d) => d),
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('TransactionLinkList', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(TransactionLinkList, { localVue, mocks, propsData })
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the API', () => {
|
||||||
|
expect(apolloQueryMock).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
query: listTransactionLinksAdmin,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 5,
|
||||||
|
userId: 42,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has 4 items in the table', () => {
|
||||||
|
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has pagination buttons', () => {
|
||||||
|
expect(wrapper.findComponent({ name: 'BPagination' }).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('next page', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the API again', () => {
|
||||||
|
expect(apolloQueryMock).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
query: listTransactionLinksAdmin,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 5,
|
||||||
|
userId: 42,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('server response with error', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
apolloQueryMock.mockRejectedValue({ message: 'Oh no!' })
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts an error message', () => {
|
||||||
|
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
105
admin/src/components/TransactionLinkList.vue
Normal file
105
admin/src/components/TransactionLinkList.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div class="transaction-link-list">
|
||||||
|
<div v-if="items.length > 0">
|
||||||
|
<div class="h3">{{ $t('transactionlink.name') }}</div>
|
||||||
|
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||||
|
</div>
|
||||||
|
<b-pagination
|
||||||
|
pills
|
||||||
|
size="lg"
|
||||||
|
v-model="currentPage"
|
||||||
|
:per-page="perPage"
|
||||||
|
:total-rows="rows"
|
||||||
|
align="center"
|
||||||
|
></b-pagination>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
|
||||||
|
export default {
|
||||||
|
name: 'TransactionLinkList',
|
||||||
|
props: {
|
||||||
|
userId: { type: Number, required: true },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
rows: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 5,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getListTransactionLinks() {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: listTransactionLinksAdmin,
|
||||||
|
variables: {
|
||||||
|
currentPage: this.currentPage,
|
||||||
|
pageSize: this.perPage,
|
||||||
|
userId: this.userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.rows = result.data.listTransactionLinksAdmin.linkCount
|
||||||
|
this.items = result.data.listTransactionLinksAdmin.linkList
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toastError(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fields() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'createdAt',
|
||||||
|
label: this.$t('transactionlink.created'),
|
||||||
|
formatter: (value, key, item) => {
|
||||||
|
return this.$d(new Date(value))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'amount',
|
||||||
|
label: this.$t('transactionlist.amount'),
|
||||||
|
formatter: (value, key, item) => {
|
||||||
|
return `${value} GDD`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ key: 'memo', label: this.$t('transactionlist.memo') },
|
||||||
|
{
|
||||||
|
key: 'validUntil',
|
||||||
|
label: this.$t('transactionlink.valid_until'),
|
||||||
|
formatter: (value, key, item) => {
|
||||||
|
return this.$d(new Date(value))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'status',
|
||||||
|
formatter: (value, key, item) => {
|
||||||
|
// deleted
|
||||||
|
if (item.deletedAt) return this.$t('deleted') + ': ' + this.$d(new Date(item.deletedAt))
|
||||||
|
// redeemed
|
||||||
|
if (item.redeemedAt)
|
||||||
|
return this.$t('redeemed') + ': ' + this.$d(new Date(item.redeemedAt))
|
||||||
|
// expired
|
||||||
|
if (new Date() > new Date(item.validUntil))
|
||||||
|
return this.$t('expired') + ': ' + this.$d(new Date(item.validUntil))
|
||||||
|
// open
|
||||||
|
return this.$t('open')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getListTransactionLinks()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentPage() {
|
||||||
|
this.getListTransactionLinks()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,19 +1,7 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const createPendingCreation = gql`
|
export const createPendingCreation = gql`
|
||||||
mutation (
|
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
$email: String!
|
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
|
||||||
$amount: Float!
|
|
||||||
$memo: String!
|
|
||||||
$creationDate: String!
|
|
||||||
$moderator: Int!
|
|
||||||
) {
|
|
||||||
createPendingCreation(
|
|
||||||
email: $email
|
|
||||||
amount: $amount
|
|
||||||
memo: $memo
|
|
||||||
creationDate: $creationDate
|
|
||||||
moderator: $moderator
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
20
admin/src/graphql/listTransactionLinksAdmin.js
Normal file
20
admin/src/graphql/listTransactionLinksAdmin.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const listTransactionLinksAdmin = gql`
|
||||||
|
query ($currentPage: Int = 1, $pageSize: Int = 5, $userId: Int!) {
|
||||||
|
listTransactionLinksAdmin(currentPage: $currentPage, pageSize: $pageSize, userId: $userId) {
|
||||||
|
linkCount
|
||||||
|
linkList {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
holdAvailableAmount
|
||||||
|
memo
|
||||||
|
code
|
||||||
|
createdAt
|
||||||
|
validUntil
|
||||||
|
redeemedAt
|
||||||
|
deletedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -5,15 +5,13 @@ export const searchUsers = gql`
|
|||||||
$searchText: String!
|
$searchText: String!
|
||||||
$currentPage: Int
|
$currentPage: Int
|
||||||
$pageSize: Int
|
$pageSize: Int
|
||||||
$notActivated: Boolean
|
$filters: SearchUsersFiltersInput
|
||||||
$isDeleted: Boolean
|
|
||||||
) {
|
) {
|
||||||
searchUsers(
|
searchUsers(
|
||||||
searchText: $searchText
|
searchText: $searchText
|
||||||
currentPage: $currentPage
|
currentPage: $currentPage
|
||||||
pageSize: $pageSize
|
pageSize: $pageSize
|
||||||
notActivated: $notActivated
|
filters: $filters
|
||||||
isDeleted: $isDeleted
|
|
||||||
) {
|
) {
|
||||||
userCount
|
userCount
|
||||||
userList {
|
userList {
|
||||||
|
|||||||
@ -1,27 +1,18 @@
|
|||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
export const updatePendingCreation = gql`
|
export const updatePendingCreation = gql`
|
||||||
mutation (
|
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
$id: Int!
|
|
||||||
$email: String!
|
|
||||||
$amount: Float!
|
|
||||||
$memo: String!
|
|
||||||
$creationDate: String!
|
|
||||||
$moderator: Int!
|
|
||||||
) {
|
|
||||||
updatePendingCreation(
|
updatePendingCreation(
|
||||||
id: $id
|
id: $id
|
||||||
email: $email
|
email: $email
|
||||||
amount: $amount
|
amount: $amount
|
||||||
memo: $memo
|
memo: $memo
|
||||||
creationDate: $creationDate
|
creationDate: $creationDate
|
||||||
moderator: $moderator
|
|
||||||
) {
|
) {
|
||||||
amount
|
amount
|
||||||
date
|
date
|
||||||
memo
|
memo
|
||||||
creation
|
creation
|
||||||
moderator
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"all_emails": "Alle Nutzer",
|
"all_emails": "Alle Nutzer",
|
||||||
"back": "zurück",
|
"back": "zurück",
|
||||||
"creation": "Schöpfung",
|
"creation": "Schöpfung",
|
||||||
|
"creationList": "Schöpfungsliste",
|
||||||
"creation_form": {
|
"creation_form": {
|
||||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||||
"creation_for": "Aktives Grundeinkommen für",
|
"creation_for": "Aktives Grundeinkommen für",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"enabled": "aktiviert",
|
"enabled": "aktiviert",
|
||||||
"error": "Fehler",
|
"error": "Fehler",
|
||||||
|
"expired": "abgelaufen",
|
||||||
"e_mail": "E-Mail",
|
"e_mail": "E-Mail",
|
||||||
"firstname": "Vorname",
|
"firstname": "Vorname",
|
||||||
"footer": {
|
"footer": {
|
||||||
@ -56,6 +58,7 @@
|
|||||||
"user_search": "Nutzersuche"
|
"user_search": "Nutzersuche"
|
||||||
},
|
},
|
||||||
"not_open_creations": "Keine offenen Schöpfungen",
|
"not_open_creations": "Keine offenen Schöpfungen",
|
||||||
|
"open": "offen",
|
||||||
"open_creations": "Offene Schöpfungen",
|
"open_creations": "Offene Schöpfungen",
|
||||||
"overlay": {
|
"overlay": {
|
||||||
"confirm": {
|
"confirm": {
|
||||||
@ -66,6 +69,7 @@
|
|||||||
"yes": "Ja, Schöpfung bestätigen und speichern!"
|
"yes": "Ja, Schöpfung bestätigen und speichern!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redeemed": "eingelöst",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
|
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
|
||||||
"remove_all": "alle Nutzer entfernen",
|
"remove_all": "alle Nutzer entfernen",
|
||||||
@ -73,6 +77,11 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"success": "Erfolg",
|
"success": "Erfolg",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
|
"transactionlink": {
|
||||||
|
"created": "Erstellt",
|
||||||
|
"name": "Transaktion-Links",
|
||||||
|
"valid_until": "Gültig bis"
|
||||||
|
},
|
||||||
"transactionlist": {
|
"transactionlist": {
|
||||||
"amount": "Betrag",
|
"amount": "Betrag",
|
||||||
"balanceDate": "Schöpfungsdatum",
|
"balanceDate": "Schöpfungsdatum",
|
||||||
@ -92,7 +101,6 @@
|
|||||||
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
|
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
|
||||||
"text_true": " Die Email wurde bestätigt."
|
"text_true": " Die Email wurde bestätigt."
|
||||||
},
|
},
|
||||||
"userIsDeleted": "Der Nutzer ist gelöscht. Es können keine GDD mehr geschöpft werden.",
|
|
||||||
"user_deleted": "Nutzer ist gelöscht.",
|
"user_deleted": "Nutzer ist gelöscht.",
|
||||||
"user_recovered": "Nutzer ist wiederhergestellt.",
|
"user_recovered": "Nutzer ist wiederhergestellt.",
|
||||||
"user_search": "Nutzer-Suche"
|
"user_search": "Nutzer-Suche"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"all_emails": "All users",
|
"all_emails": "All users",
|
||||||
"back": "back",
|
"back": "back",
|
||||||
"creation": "Creation",
|
"creation": "Creation",
|
||||||
|
"creationList": "Creation list",
|
||||||
"creation_form": {
|
"creation_form": {
|
||||||
"creation_failed": "Could not create pending creation for {email}",
|
"creation_failed": "Could not create pending creation for {email}",
|
||||||
"creation_for": "Active Basic Income for",
|
"creation_for": "Active Basic Income for",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"enabled": "enabled",
|
"enabled": "enabled",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
|
"expired": "expired",
|
||||||
"e_mail": "E-mail",
|
"e_mail": "E-mail",
|
||||||
"firstname": "Firstname",
|
"firstname": "Firstname",
|
||||||
"footer": {
|
"footer": {
|
||||||
@ -56,6 +58,7 @@
|
|||||||
"user_search": "User search"
|
"user_search": "User search"
|
||||||
},
|
},
|
||||||
"not_open_creations": "No open creations",
|
"not_open_creations": "No open creations",
|
||||||
|
"open": "open",
|
||||||
"open_creations": "Open creations",
|
"open_creations": "Open creations",
|
||||||
"overlay": {
|
"overlay": {
|
||||||
"confirm": {
|
"confirm": {
|
||||||
@ -66,6 +69,7 @@
|
|||||||
"yes": "Yes, confirm and save creation!"
|
"yes": "Yes, confirm and save creation!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redeemed": "redeemed",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
||||||
"remove_all": "Remove all users",
|
"remove_all": "Remove all users",
|
||||||
@ -73,6 +77,11 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
|
"transactionlink": {
|
||||||
|
"created": "Created",
|
||||||
|
"name": "Transaction links",
|
||||||
|
"valid_until": "Valid until"
|
||||||
|
},
|
||||||
"transactionlist": {
|
"transactionlist": {
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"balanceDate": "Creation date",
|
"balanceDate": "Creation date",
|
||||||
@ -92,7 +101,6 @@
|
|||||||
"text_false": "The last email was sent to the member ({email}) on {date}.",
|
"text_false": "The last email was sent to the member ({email}) on {date}.",
|
||||||
"text_true": "The email was confirmed."
|
"text_true": "The email was confirmed."
|
||||||
},
|
},
|
||||||
"userIsDeleted": "The user is deleted. No more GDD can be created.",
|
|
||||||
"user_deleted": "User is deleted.",
|
"user_deleted": "User is deleted.",
|
||||||
"user_recovered": "User is recovered.",
|
"user_recovered": "User is recovered.",
|
||||||
"user_search": "User search"
|
"user_search": "User search"
|
||||||
|
|||||||
@ -71,6 +71,10 @@ describe('Creation', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
|
filters: {
|
||||||
|
filterByActivated: true,
|
||||||
|
filterByDeleted: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -269,6 +273,10 @@ describe('Creation', () => {
|
|||||||
searchText: 'XX',
|
searchText: 'XX',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
|
filters: {
|
||||||
|
filterByActivated: true,
|
||||||
|
filterByDeleted: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -284,6 +292,10 @@ describe('Creation', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
|
filters: {
|
||||||
|
filterByActivated: true,
|
||||||
|
filterByDeleted: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -299,6 +311,10 @@ describe('Creation', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 2,
|
currentPage: 2,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
|
filters: {
|
||||||
|
filterByActivated: true,
|
||||||
|
filterByDeleted: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -102,6 +102,10 @@ export default {
|
|||||||
searchText: this.criteria,
|
searchText: this.criteria,
|
||||||
currentPage: this.currentPage,
|
currentPage: this.currentPage,
|
||||||
pageSize: this.perPage,
|
pageSize: this.perPage,
|
||||||
|
filters: {
|
||||||
|
filterByActivated: true,
|
||||||
|
filterByDeleted: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -7,7 +7,7 @@ const localVue = global.localVue
|
|||||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
searchUsers: {
|
searchUsers: {
|
||||||
userCount: 1,
|
userCount: 4,
|
||||||
userList: [
|
userList: [
|
||||||
{
|
{
|
||||||
userId: 1,
|
userId: 1,
|
||||||
@ -82,8 +82,10 @@ describe('UserSearch', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
notActivated: false,
|
filters: {
|
||||||
isDeleted: false,
|
filterByActivated: null,
|
||||||
|
filterByDeleted: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -101,8 +103,10 @@ describe('UserSearch', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
notActivated: true,
|
filters: {
|
||||||
isDeleted: false,
|
filterByActivated: false,
|
||||||
|
filterByDeleted: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -121,8 +125,10 @@ describe('UserSearch', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
notActivated: false,
|
filters: {
|
||||||
isDeleted: true,
|
filterByActivated: null,
|
||||||
|
filterByDeleted: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -141,8 +147,10 @@ describe('UserSearch', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 2,
|
currentPage: 2,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
notActivated: false,
|
filters: {
|
||||||
isDeleted: false,
|
filterByActivated: null,
|
||||||
|
filterByDeleted: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -161,8 +169,10 @@ describe('UserSearch', () => {
|
|||||||
searchText: 'search string',
|
searchText: 'search string',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
notActivated: false,
|
filters: {
|
||||||
isDeleted: false,
|
filterByActivated: null,
|
||||||
|
filterByDeleted: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -178,8 +188,10 @@ describe('UserSearch', () => {
|
|||||||
searchText: '',
|
searchText: '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
notActivated: false,
|
filters: {
|
||||||
isDeleted: false,
|
filterByActivated: null,
|
||||||
|
filterByDeleted: null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,11 +3,23 @@
|
|||||||
<div class="user-search-first-div">
|
<div class="user-search-first-div">
|
||||||
<b-button class="unconfirmedRegisterMails" variant="light" @click="unconfirmedRegisterMails">
|
<b-button class="unconfirmedRegisterMails" variant="light" @click="unconfirmedRegisterMails">
|
||||||
<b-icon icon="envelope" variant="danger"></b-icon>
|
<b-icon icon="envelope" variant="danger"></b-icon>
|
||||||
{{ filterCheckedEmails ? $t('all_emails') : $t('unregistered_emails') }}
|
{{
|
||||||
|
filterByActivated === null
|
||||||
|
? $t('all_emails')
|
||||||
|
: filterByActivated === false
|
||||||
|
? $t('unregistered_emails')
|
||||||
|
: ''
|
||||||
|
}}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button class="deletedUserSearch" variant="light" @click="deletedUserSearch">
|
<b-button class="deletedUserSearch" variant="light" @click="deletedUserSearch">
|
||||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||||
{{ filterDeletedUser ? $t('all_emails') : $t('deleted_user') }}
|
{{
|
||||||
|
filterByDeleted === null
|
||||||
|
? $t('all_emails')
|
||||||
|
: filterByDeleted === true
|
||||||
|
? $t('deleted_user')
|
||||||
|
: ''
|
||||||
|
}}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
<label>{{ $t('user_search') }}</label>
|
<label>{{ $t('user_search') }}</label>
|
||||||
@ -60,8 +72,8 @@ export default {
|
|||||||
searchResult: [],
|
searchResult: [],
|
||||||
massCreation: [],
|
massCreation: [],
|
||||||
criteria: '',
|
criteria: '',
|
||||||
filterCheckedEmails: false,
|
filterByActivated: null,
|
||||||
filterDeletedUser: false,
|
filterByDeleted: null,
|
||||||
rows: 0,
|
rows: 0,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
perPage: 25,
|
perPage: 25,
|
||||||
@ -70,11 +82,11 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
unconfirmedRegisterMails() {
|
unconfirmedRegisterMails() {
|
||||||
this.filterCheckedEmails = !this.filterCheckedEmails
|
this.filterByActivated = this.filterByActivated === null ? false : null
|
||||||
this.getUsers()
|
this.getUsers()
|
||||||
},
|
},
|
||||||
deletedUserSearch() {
|
deletedUserSearch() {
|
||||||
this.filterDeletedUser = !this.filterDeletedUser
|
this.filterByDeleted = this.filterByDeleted === null ? true : null
|
||||||
this.getUsers()
|
this.getUsers()
|
||||||
},
|
},
|
||||||
getUsers() {
|
getUsers() {
|
||||||
@ -85,9 +97,12 @@ export default {
|
|||||||
searchText: this.criteria,
|
searchText: this.criteria,
|
||||||
currentPage: this.currentPage,
|
currentPage: this.currentPage,
|
||||||
pageSize: this.perPage,
|
pageSize: this.perPage,
|
||||||
notActivated: this.filterCheckedEmails,
|
filters: {
|
||||||
isDeleted: this.filterDeletedUser,
|
filterByActivated: this.filterByActivated,
|
||||||
|
filterByDeleted: this.filterByDeleted,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.rows = result.data.searchUsers.userCount
|
this.rows = result.data.searchUsers.userCount
|
||||||
|
|||||||
@ -4688,6 +4688,13 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
|
|||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
cross-env@^7.0.3:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
||||||
|
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
|
||||||
|
dependencies:
|
||||||
|
cross-spawn "^7.0.1"
|
||||||
|
|
||||||
cross-spawn@^5.0.1:
|
cross-spawn@^5.0.1:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
||||||
@ -4708,7 +4715,7 @@ cross-spawn@^6.0.0:
|
|||||||
shebang-command "^1.2.0"
|
shebang-command "^1.2.0"
|
||||||
which "^1.2.9"
|
which "^1.2.9"
|
||||||
|
|
||||||
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
|
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
CONFIG_VERSION=v1.2022-03-18
|
CONFIG_VERSION=v6.2022-04-21
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=4000
|
||||||
JWT_SECRET=secret123
|
JWT_SECRET=secret123
|
||||||
JWT_EXPIRES_IN=10m
|
JWT_EXPIRES_IN=30m
|
||||||
GRAPHIQL=false
|
GRAPHIQL=false
|
||||||
GDT_API_URL=https://gdt.gradido.net
|
GDT_API_URL=https://gdt.gradido.net
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
|||||||
COMMUNITY_NAME=Gradido Entwicklung
|
COMMUNITY_NAME=Gradido Entwicklung
|
||||||
COMMUNITY_URL=http://localhost/
|
COMMUNITY_URL=http://localhost/
|
||||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||||
|
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
|
||||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||||
|
|
||||||
# Login Server
|
# Login Server
|
||||||
@ -41,8 +42,15 @@ EMAIL_PASSWORD=xxx
|
|||||||
EMAIL_SMTP_URL=gmail.com
|
EMAIL_SMTP_URL=gmail.com
|
||||||
EMAIL_SMTP_PORT=587
|
EMAIL_SMTP_PORT=587
|
||||||
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
|
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{optin}{code}
|
||||||
EMAIL_LINK_SETPASSWORD=http://localhost/reset/{optin}
|
EMAIL_LINK_SETPASSWORD=http://localhost/reset-password/{optin}
|
||||||
EMAIL_CODE_VALID_TIME=10
|
EMAIL_LINK_FORGOTPASSWORD=http://localhost/forgot-password
|
||||||
|
EMAIL_LINK_OVERVIEW=http://localhost/overview
|
||||||
|
EMAIL_CODE_VALID_TIME=1440
|
||||||
|
EMAIL_CODE_REQUEST_TIME=10
|
||||||
|
|
||||||
# Webhook
|
# Webhook
|
||||||
WEBHOOK_ELOPAGE_SECRET=secret
|
WEBHOOK_ELOPAGE_SECRET=secret
|
||||||
|
|
||||||
|
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||||
|
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||||
|
# LOG_LEVEL=info
|
||||||
|
|||||||
@ -2,7 +2,7 @@ CONFIG_VERSION=$BACKEND_CONFIG_VERSION
|
|||||||
|
|
||||||
# Server
|
# Server
|
||||||
JWT_SECRET=$JWT_SECRET
|
JWT_SECRET=$JWT_SECRET
|
||||||
JWT_EXPIRES_IN=10m
|
JWT_EXPIRES_IN=$JWT_EXPIRES_IN
|
||||||
GRAPHIQL=false
|
GRAPHIQL=false
|
||||||
GDT_API_URL=$GDT_API_URL
|
GDT_API_URL=$GDT_API_URL
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
|
|||||||
COMMUNITY_NAME=$COMMUNITY_NAME
|
COMMUNITY_NAME=$COMMUNITY_NAME
|
||||||
COMMUNITY_URL=$COMMUNITY_URL
|
COMMUNITY_URL=$COMMUNITY_URL
|
||||||
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
||||||
|
COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL
|
||||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||||
|
|
||||||
# Login Server
|
# Login Server
|
||||||
@ -33,7 +34,6 @@ LOGIN_APP_SECRET=21ffbbc616fe
|
|||||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||||
|
|
||||||
# EMail
|
# EMail
|
||||||
RESEND_TIME=10
|
|
||||||
EMAIL=$EMAIL
|
EMAIL=$EMAIL
|
||||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||||
EMAIL_SENDER=$EMAIL_SENDER
|
EMAIL_SENDER=$EMAIL_SENDER
|
||||||
@ -42,7 +42,9 @@ EMAIL_SMTP_URL=$EMAIL_SMTP_URL
|
|||||||
EMAIL_SMTP_PORT=587
|
EMAIL_SMTP_PORT=587
|
||||||
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
|
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
|
||||||
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
|
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
|
||||||
RESEND_TIME=10
|
EMAIL_LINK_OVERVIEW=$EMAIL_LINK_OVERVIEW
|
||||||
|
EMAIL_CODE_VALID_TIME=$EMAIL_CODE_VALID_TIME
|
||||||
|
EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
|
||||||
|
|
||||||
# Webhook
|
# Webhook
|
||||||
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
||||||
@ -5,4 +5,5 @@ module.exports = {
|
|||||||
trailingComma: "all",
|
trailingComma: "all",
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
|
endOfLine: "auto",
|
||||||
};
|
};
|
||||||
|
|||||||
24
backend/README.md
Normal file
24
backend/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# backend
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Seed DB
|
||||||
|
```
|
||||||
|
yarn seed
|
||||||
|
```
|
||||||
|
Deletes all data in database. Then seeds data in database.
|
||||||
|
|
||||||
|
|
||||||
|
## Seeded Users
|
||||||
|
|
||||||
|
| email | password | admin | email checked | deleted |
|
||||||
|
|------------------------|------------|---------|---------------|---------|
|
||||||
|
| peter@lustig.de | `Aa12345_` | `true` | `true` | `false` |
|
||||||
|
| bibi@bloxberg.de | `Aa12345_` | `false` | `true` | `false` |
|
||||||
|
| raeuber@hotzenplotz.de | `Aa12345_` | `false` | `true` | `false` |
|
||||||
|
| bob@baumeister.de | `Aa12345_` | `false` | `true` | `false` |
|
||||||
|
| garrick@ollivander.com | | `false` | `false` | `false` |
|
||||||
|
| stephen@hawking.uk | `Aa12345_` | `false` | `true` | `true` |
|
||||||
66
backend/log4js-config.json
Normal file
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",
|
"name": "gradido-backend",
|
||||||
"version": "1.6.6",
|
"version": "1.8.3",
|
||||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/backend",
|
"repository": "https://github.com/gradido/gradido/backend",
|
||||||
@ -10,21 +10,21 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc --build",
|
"build": "tsc --build",
|
||||||
"clean": "tsc --build --clean",
|
"clean": "tsc --build --clean",
|
||||||
"start": "TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||||
"dev": "TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
|
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
|
||||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||||
"test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
|
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
|
||||||
"seed": "TZ=UTC ts-node -r tsconfig-paths/register src/seeds/index.ts"
|
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/lodash.clonedeep": "^4.5.6",
|
"@types/lodash.clonedeep": "^4.5.6",
|
||||||
"apollo-log": "^1.1.0",
|
|
||||||
"apollo-server-express": "^2.25.2",
|
"apollo-server-express": "^2.25.2",
|
||||||
"apollo-server-testing": "^2.25.2",
|
"apollo-server-testing": "^2.25.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"class-validator": "^0.13.1",
|
"class-validator": "^0.13.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"decimal.js-light": "^2.5.1",
|
"decimal.js-light": "^2.5.1",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
@ -32,6 +32,7 @@
|
|||||||
"jest": "^27.2.4",
|
"jest": "^27.2.4",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"log4js": "^6.4.6",
|
||||||
"mysql2": "^2.3.0",
|
"mysql2": "^2.3.0",
|
||||||
"nodemailer": "^6.6.5",
|
"nodemailer": "^6.6.5",
|
||||||
"random-bigint": "^0.0.1",
|
"random-bigint": "^0.0.1",
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const apiPost = async (url: string, payload: unknown): Promise<any> => {
|
export const apiPost = async (url: string, payload: unknown): Promise<any> => {
|
||||||
|
logger.trace('POST: url=' + url + ' payload=' + payload)
|
||||||
return axios
|
return axios
|
||||||
.post(url, payload)
|
.post(url, payload)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
logger.trace('POST-Response: result=' + result)
|
||||||
if (result.status !== 200) {
|
if (result.status !== 200) {
|
||||||
throw new Error('HTTP Status Error ' + result.status)
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const apiGet = async (url: string): Promise<any> => {
|
export const apiGet = async (url: string): Promise<any> => {
|
||||||
|
logger.trace('GET: url=' + url)
|
||||||
return axios
|
return axios
|
||||||
.get(url)
|
.get(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
logger.trace('GET-Response: result=' + result)
|
||||||
if (result.status !== 200) {
|
if (result.status !== 200) {
|
||||||
throw new Error('HTTP Status Error ' + result.status)
|
throw new Error('HTTP Status Error ' + result.status)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export enum RIGHTS {
|
|||||||
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
|
||||||
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
|
||||||
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
|
||||||
|
GDT_BALANCE = 'GDT_BALANCE',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
CREATE_PENDING_CREATION = 'CREATE_PENDING_CREATION',
|
||||||
@ -35,4 +36,5 @@ export enum RIGHTS {
|
|||||||
DELETE_USER = 'DELETE_USER',
|
DELETE_USER = 'DELETE_USER',
|
||||||
UNDELETE_USER = 'UNDELETE_USER',
|
UNDELETE_USER = 'UNDELETE_USER',
|
||||||
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
|
||||||
|
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export const ROLE_USER = new Role('user', [
|
|||||||
RIGHTS.DELETE_TRANSACTION_LINK,
|
RIGHTS.DELETE_TRANSACTION_LINK,
|
||||||
RIGHTS.REDEEM_TRANSACTION_LINK,
|
RIGHTS.REDEEM_TRANSACTION_LINK,
|
||||||
RIGHTS.LIST_TRANSACTION_LINKS,
|
RIGHTS.LIST_TRANSACTION_LINKS,
|
||||||
|
RIGHTS.GDT_BALANCE,
|
||||||
])
|
])
|
||||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||||
|
|
||||||
|
|||||||
@ -10,11 +10,14 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0033-add_referrer_id',
|
DB_VERSION: '0036-unique_previous_in_transactions',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
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: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v1.2022-03-18',
|
EXPECTED: 'v6.2022-04-21',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -22,7 +25,7 @@ const constants = {
|
|||||||
const server = {
|
const server = {
|
||||||
PORT: process.env.PORT || 4000,
|
PORT: process.env.PORT || 4000,
|
||||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '30m',
|
||||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||||
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net',
|
||||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||||
@ -50,6 +53,7 @@ const community = {
|
|||||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
|
||||||
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
|
||||||
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
|
||||||
|
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
|
||||||
COMMUNITY_DESCRIPTION:
|
COMMUNITY_DESCRIPTION:
|
||||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
}
|
}
|
||||||
@ -70,8 +74,16 @@ const email = {
|
|||||||
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/checkEmail/{optin}{code}',
|
||||||
EMAIL_LINK_SETPASSWORD:
|
EMAIL_LINK_SETPASSWORD:
|
||||||
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
|
process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/reset-password/{optin}',
|
||||||
|
EMAIL_LINK_FORGOTPASSWORD:
|
||||||
|
process.env.EMAIL_LINK_FORGOTPASSWORD || 'http://localhost/forgot-password',
|
||||||
|
EMAIL_LINK_OVERVIEW: process.env.EMAIL_LINK_OVERVIEW || 'http://localhost/overview',
|
||||||
|
// time in minutes a optin code is valid
|
||||||
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
|
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
|
||||||
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 10
|
? parseInt(process.env.EMAIL_CODE_VALID_TIME) || 1440
|
||||||
|
: 1440,
|
||||||
|
// time in minutes that must pass to request a new optin code
|
||||||
|
EMAIL_CODE_REQUEST_TIME: process.env.EMAIL_CODE_REQUEST_TIME
|
||||||
|
? parseInt(process.env.EMAIL_CODE_REQUEST_TIME) || 10
|
||||||
: 10,
|
: 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ArgsType, Field, Float, InputType, Int } from 'type-graphql'
|
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@ -6,15 +7,12 @@ export default class CreatePendingCreationArgs {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
@Field(() => Float)
|
@Field(() => Decimal)
|
||||||
amount: number
|
amount: Decimal
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
memo: string
|
memo: string
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
creationDate: string
|
creationDate: string
|
||||||
|
|
||||||
@Field(() => Int)
|
|
||||||
moderator: number
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ArgsType, Field, Int } from 'type-graphql'
|
import { ArgsType, Field, Int } from 'type-graphql'
|
||||||
|
import SearchUsersFilters from '@arg/SearchUsersFilters'
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export default class SearchUsersArgs {
|
export default class SearchUsersArgs {
|
||||||
@ -11,9 +12,6 @@ export default class SearchUsersArgs {
|
|||||||
@Field(() => Int, { nullable: true })
|
@Field(() => Int, { nullable: true })
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
|
|
||||||
@Field(() => Boolean, { nullable: true })
|
@Field(() => SearchUsersFilters, { nullable: true })
|
||||||
notActivated?: boolean
|
filters: SearchUsersFilters
|
||||||
|
|
||||||
@Field(() => Boolean, { nullable: true })
|
|
||||||
isDeleted?: boolean
|
|
||||||
}
|
}
|
||||||
|
|||||||
11
backend/src/graphql/arg/SearchUsersFilters.ts
Normal file
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
|
||||||
|
}
|
||||||
13
backend/src/graphql/arg/TransactionLinkFilters.ts
Normal file
13
backend/src/graphql/arg/TransactionLinkFilters.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ArgsType, Field } from 'type-graphql'
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export default class TransactionLinkFilters {
|
||||||
|
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||||
|
filterByDeleted?: boolean
|
||||||
|
|
||||||
|
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||||
|
filterByExpired?: boolean
|
||||||
|
|
||||||
|
@Field(() => Boolean, { nullable: true, defaultValue: true })
|
||||||
|
filterByRedeemed?: boolean
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { ArgsType, Field, Float, Int } from 'type-graphql'
|
import { ArgsType, Field, Int } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export default class UpdatePendingCreationArgs {
|
export default class UpdatePendingCreationArgs {
|
||||||
@ -8,15 +9,12 @@ export default class UpdatePendingCreationArgs {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
@Field(() => Float)
|
@Field(() => Decimal)
|
||||||
amount: number
|
amount: Decimal
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
memo: string
|
memo: string
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
creationDate: string
|
creationDate: string
|
||||||
|
|
||||||
@Field(() => Int)
|
|
||||||
moderator: number
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { RIGHTS } from '@/auth/RIGHTS'
|
|||||||
import { getCustomRepository } from '@dbTools/typeorm'
|
import { getCustomRepository } from '@dbTools/typeorm'
|
||||||
import { UserRepository } from '@repository/User'
|
import { UserRepository } from '@repository/User'
|
||||||
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
||||||
import { ServerUser } from '@entity/ServerUser'
|
|
||||||
|
|
||||||
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
||||||
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
||||||
@ -36,8 +35,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
|||||||
try {
|
try {
|
||||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
context.user = user
|
context.user = user
|
||||||
const countServerUsers = await ServerUser.count({ email: user.email })
|
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
|
||||||
context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
|
|
||||||
} catch {
|
} catch {
|
||||||
// in case the database query fails (user deleted)
|
// in case the database query fails (user deleted)
|
||||||
throw new Error('401 Unauthorized')
|
throw new Error('401 Unauthorized')
|
||||||
|
|||||||
11
backend/src/graphql/enum/OptInType.ts
Normal file
11
backend/src/graphql/enum/OptInType.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { registerEnumType } from 'type-graphql'
|
||||||
|
|
||||||
|
export enum OptInType {
|
||||||
|
EMAIL_OPT_IN_REGISTER = 1,
|
||||||
|
EMAIL_OPT_IN_RESET_PASSWORD = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
registerEnumType(OptInType, {
|
||||||
|
name: 'OptInType', // this one is mandatory
|
||||||
|
description: 'Type of the email optin', // this one is optional
|
||||||
|
})
|
||||||
@ -1,22 +1,32 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Balance {
|
export class Balance {
|
||||||
constructor(json: any) {
|
constructor(data: {
|
||||||
this.balance = json.balance
|
balance: Decimal
|
||||||
this.decay = json.decay
|
balanceGDT: number | null
|
||||||
this.decayDate = json.decay_date
|
count: number
|
||||||
|
linkCount: number
|
||||||
|
}) {
|
||||||
|
this.balance = data.balance
|
||||||
|
this.balanceGDT = data.balanceGDT || null
|
||||||
|
this.count = data.count
|
||||||
|
this.linkCount = data.linkCount
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the actual balance, decay included
|
||||||
@Field(() => Decimal)
|
@Field(() => Decimal)
|
||||||
balance: Decimal
|
balance: Decimal
|
||||||
|
|
||||||
@Field(() => Decimal)
|
@Field(() => Number, { nullable: true })
|
||||||
decay: Decimal
|
balanceGDT: number | null
|
||||||
|
|
||||||
@Field(() => Date)
|
// the count of all transactions
|
||||||
decayDate: Date
|
@Field(() => Number)
|
||||||
|
count: number
|
||||||
|
|
||||||
|
// the count of transaction links
|
||||||
|
@Field(() => Number)
|
||||||
|
linkCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,21 @@
|
|||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
interface DecayInterface {
|
||||||
|
balance: Decimal
|
||||||
|
decay: Decimal
|
||||||
|
roundedDecay: Decimal
|
||||||
|
start: Date | null
|
||||||
|
end: Date | null
|
||||||
|
duration: number | null
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Decay {
|
export class Decay {
|
||||||
constructor(
|
constructor({ balance, decay, roundedDecay, start, end, duration }: DecayInterface) {
|
||||||
balance: Decimal,
|
|
||||||
decay: Decimal,
|
|
||||||
start: Date | null,
|
|
||||||
end: Date | null,
|
|
||||||
duration: number | null,
|
|
||||||
) {
|
|
||||||
this.balance = balance
|
this.balance = balance
|
||||||
this.decay = decay
|
this.decay = decay
|
||||||
|
this.roundedDecay = roundedDecay
|
||||||
this.start = start
|
this.start = start
|
||||||
this.end = end
|
this.end = end
|
||||||
this.duration = duration
|
this.duration = duration
|
||||||
@ -23,6 +27,9 @@ export class Decay {
|
|||||||
@Field(() => Decimal)
|
@Field(() => Decimal)
|
||||||
decay: Decimal
|
decay: Decimal
|
||||||
|
|
||||||
|
@Field(() => Decimal)
|
||||||
|
roundedDecay: Decimal
|
||||||
|
|
||||||
@Field(() => Date, { nullable: true })
|
@Field(() => Date, { nullable: true })
|
||||||
start: Date | null
|
start: Date | null
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class PendingCreation {
|
export class PendingCreation {
|
||||||
@ -23,12 +24,12 @@ export class PendingCreation {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
memo: string
|
memo: string
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
amount: number
|
amount: Decimal
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
moderator: number
|
moderator: number
|
||||||
|
|
||||||
@Field(() => [Number])
|
@Field(() => [Decimal])
|
||||||
creation: number[]
|
creation: Decimal[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,19 +12,31 @@ export class Transaction {
|
|||||||
this.user = user
|
this.user = user
|
||||||
this.previous = transaction.previous
|
this.previous = transaction.previous
|
||||||
this.typeId = transaction.typeId
|
this.typeId = transaction.typeId
|
||||||
this.amount = transaction.amount
|
this.amount = transaction.amount.toDecimalPlaces(2, Decimal.ROUND_DOWN)
|
||||||
this.balance = transaction.balance
|
this.balance = transaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN)
|
||||||
this.balanceDate = transaction.balanceDate
|
this.balanceDate = transaction.balanceDate
|
||||||
if (!transaction.decayStart) {
|
if (!transaction.decayStart) {
|
||||||
this.decay = new Decay(transaction.balance, new Decimal(0), null, null, null)
|
// TODO: hot fix, we should separate decay calculation from decay graphql model
|
||||||
|
this.decay = new Decay({
|
||||||
|
balance: transaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN),
|
||||||
|
decay: new Decimal(0),
|
||||||
|
roundedDecay: new Decimal(0),
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
duration: null,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.decay = new Decay(
|
this.decay = new Decay({
|
||||||
transaction.balance,
|
balance: transaction.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN),
|
||||||
transaction.decay,
|
decay: transaction.decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR),
|
||||||
transaction.decayStart,
|
// TODO: add correct value when decay must be rounded in transaction context
|
||||||
transaction.balanceDate,
|
roundedDecay: new Decimal(0),
|
||||||
Math.round((transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000),
|
start: transaction.decayStart,
|
||||||
)
|
end: transaction.balanceDate,
|
||||||
|
duration: Math.round(
|
||||||
|
(transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000,
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.memo = transaction.memo
|
this.memo = transaction.memo
|
||||||
this.creationDate = transaction.creationDate
|
this.creationDate = transaction.creationDate
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { User } from './User'
|
import { User } from './User'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class TransactionLink {
|
export class TransactionLink {
|
||||||
@ -17,6 +18,7 @@ export class TransactionLink {
|
|||||||
this.deletedAt = transactionLink.deletedAt
|
this.deletedAt = transactionLink.deletedAt
|
||||||
this.redeemedAt = transactionLink.redeemedAt
|
this.redeemedAt = transactionLink.redeemedAt
|
||||||
this.redeemedBy = redeemedBy
|
this.redeemedBy = redeemedBy
|
||||||
|
this.link = CONFIG.COMMUNITY_REDEEM_URL.replace(/{code}/g, this.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
@ -51,4 +53,16 @@ export class TransactionLink {
|
|||||||
|
|
||||||
@Field(() => User, { nullable: true })
|
@Field(() => User, { nullable: true })
|
||||||
redeemedBy: User | null
|
redeemedBy: User | null
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
link: string
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class TransactionLinkResult {
|
||||||
|
@Field(() => Int)
|
||||||
|
linkCount: number
|
||||||
|
|
||||||
|
@Field(() => [TransactionLink])
|
||||||
|
linkList: TransactionLink[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,40 +1,16 @@
|
|||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
import CONFIG from '@/config'
|
|
||||||
import Decimal from 'decimal.js-light'
|
|
||||||
import { Transaction } from './Transaction'
|
import { Transaction } from './Transaction'
|
||||||
|
import { Balance } from './Balance'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class TransactionList {
|
export class TransactionList {
|
||||||
constructor(
|
constructor(balance: Balance, transactions: Transaction[]) {
|
||||||
balance: Decimal,
|
|
||||||
transactions: Transaction[],
|
|
||||||
count: number,
|
|
||||||
linkCount: number,
|
|
||||||
balanceGDT?: number | null,
|
|
||||||
decayStartBlock: Date = CONFIG.DECAY_START_TIME,
|
|
||||||
) {
|
|
||||||
this.balance = balance
|
this.balance = balance
|
||||||
this.transactions = transactions
|
this.transactions = transactions
|
||||||
this.count = count
|
|
||||||
this.linkCount = linkCount
|
|
||||||
this.balanceGDT = balanceGDT || null
|
|
||||||
this.decayStartBlock = decayStartBlock
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number, { nullable: true })
|
@Field(() => Balance)
|
||||||
balanceGDT: number | null
|
balance: Balance
|
||||||
|
|
||||||
@Field(() => Number)
|
|
||||||
count: number
|
|
||||||
|
|
||||||
@Field(() => Number)
|
|
||||||
linkCount: number
|
|
||||||
|
|
||||||
@Field(() => Decimal)
|
|
||||||
balance: Decimal
|
|
||||||
|
|
||||||
@Field(() => Date)
|
|
||||||
decayStartBlock: Date
|
|
||||||
|
|
||||||
@Field(() => [Transaction])
|
@Field(() => [Transaction])
|
||||||
transactions: Transaction[]
|
transactions: Transaction[]
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { ObjectType, Field } from 'type-graphql'
|
import { ObjectType, Field } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UpdatePendingCreation {
|
export class UpdatePendingCreation {
|
||||||
@ -8,12 +9,9 @@ export class UpdatePendingCreation {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
memo: string
|
memo: string
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Decimal)
|
||||||
amount: number
|
amount: Decimal
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => [Decimal])
|
||||||
moderator: number
|
creation: Decimal[]
|
||||||
|
|
||||||
@Field(() => [Number])
|
|
||||||
creation: number[]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,8 +14,8 @@ export class User {
|
|||||||
this.emailChecked = user.emailChecked
|
this.emailChecked = user.emailChecked
|
||||||
this.language = user.language
|
this.language = user.language
|
||||||
this.publisherId = user.publisherId
|
this.publisherId = user.publisherId
|
||||||
|
this.isAdmin = user.isAdmin
|
||||||
// TODO
|
// TODO
|
||||||
this.isAdmin = null
|
|
||||||
this.coinanimation = null
|
this.coinanimation = null
|
||||||
this.klickTipp = null
|
this.klickTipp = null
|
||||||
this.hasElopage = null
|
this.hasElopage = null
|
||||||
@ -58,11 +58,11 @@ export class User {
|
|||||||
|
|
||||||
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
// TODO this is a bit inconsistent with what we query from the database
|
// TODO this is a bit inconsistent with what we query from the database
|
||||||
// therefore all those fields are now nullable with default value null
|
// therefore all those fields are now nullable with default value null
|
||||||
@Field(() => Boolean, { nullable: true })
|
|
||||||
isAdmin: boolean | null
|
|
||||||
|
|
||||||
@Field(() => Boolean, { nullable: true })
|
@Field(() => Boolean, { nullable: true })
|
||||||
coinanimation: boolean | null
|
coinanimation: boolean | null
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { User } from '@entity/User'
|
|
||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { User } from '@entity/User'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UserAdmin {
|
export class UserAdmin {
|
||||||
constructor(user: User, creation: number[], hasElopage: boolean, emailConfirmationSend: string) {
|
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||||
this.userId = user.id
|
this.userId = user.id
|
||||||
this.email = user.email
|
this.email = user.email
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
@ -27,8 +28,8 @@ export class UserAdmin {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
lastName: string
|
lastName: string
|
||||||
|
|
||||||
@Field(() => [Number])
|
@Field(() => [Decimal])
|
||||||
creation: number[]
|
creation: Decimal[]
|
||||||
|
|
||||||
@Field(() => Boolean)
|
@Field(() => Boolean)
|
||||||
emailChecked: boolean
|
emailChecked: boolean
|
||||||
|
|||||||
1327
backend/src/graphql/resolver/AdminResolver.test.ts
Normal file
1327
backend/src/graphql/resolver/AdminResolver.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { Context, getUser } from '@/server/context'
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql'
|
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql'
|
||||||
import {
|
import {
|
||||||
getCustomRepository,
|
getCustomRepository,
|
||||||
@ -9,6 +7,8 @@ import {
|
|||||||
ObjectLiteral,
|
ObjectLiteral,
|
||||||
getConnection,
|
getConnection,
|
||||||
In,
|
In,
|
||||||
|
MoreThan,
|
||||||
|
FindOperator,
|
||||||
} from '@dbTools/typeorm'
|
} from '@dbTools/typeorm'
|
||||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||||
import { PendingCreation } from '@model/PendingCreation'
|
import { PendingCreation } from '@model/PendingCreation'
|
||||||
@ -21,6 +21,8 @@ import UpdatePendingCreationArgs from '@arg/UpdatePendingCreationArgs'
|
|||||||
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
import { Transaction } from '@model/Transaction'
|
import { Transaction } from '@model/Transaction'
|
||||||
|
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||||
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { TransactionRepository } from '@repository/Transaction'
|
import { TransactionRepository } from '@repository/Transaction'
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||||
@ -32,12 +34,16 @@ import { TransactionTypeId } from '@enum/TransactionTypeId'
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { Decay } from '@model/Decay'
|
import { Decay } from '@model/Decay'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
|
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { communityUser } from '@/util/communityUser'
|
import { communityUser } from '@/util/communityUser'
|
||||||
|
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
||||||
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
// const EMAIL_OPT_IN_REGISTER = 1
|
// const EMAIL_OPT_IN_REGISTER = 1
|
||||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||||
const MAX_CREATION_AMOUNT = 1000
|
const MAX_CREATION_AMOUNT = new Decimal(1000)
|
||||||
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
const FULL_CREATION_AVAILABLE = [MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT, MAX_CREATION_AMOUNT]
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -46,23 +52,19 @@ export class AdminResolver {
|
|||||||
@Query(() => SearchUsersResult)
|
@Query(() => SearchUsersResult)
|
||||||
async searchUsers(
|
async searchUsers(
|
||||||
@Args()
|
@Args()
|
||||||
{
|
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||||
searchText,
|
|
||||||
currentPage = 1,
|
|
||||||
pageSize = 25,
|
|
||||||
notActivated = false,
|
|
||||||
isDeleted = false,
|
|
||||||
}: SearchUsersArgs,
|
|
||||||
): Promise<SearchUsersResult> {
|
): Promise<SearchUsersResult> {
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
const userRepository = getCustomRepository(UserRepository)
|
||||||
|
|
||||||
const filterCriteria: ObjectLiteral[] = []
|
const filterCriteria: ObjectLiteral[] = []
|
||||||
if (notActivated) {
|
if (filters) {
|
||||||
filterCriteria.push({ emailChecked: false })
|
if (filters.filterByActivated !== null) {
|
||||||
|
filterCriteria.push({ emailChecked: filters.filterByActivated })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDeleted) {
|
if (filters.filterByDeleted !== null) {
|
||||||
filterCriteria.push({ deletedAt: Not(IsNull()) })
|
filterCriteria.push({ deletedAt: filters.filterByDeleted ? Not(IsNull()) : IsNull() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
|
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
|
||||||
@ -129,7 +131,7 @@ export class AdminResolver {
|
|||||||
@Mutation(() => Date, { nullable: true })
|
@Mutation(() => Date, { nullable: true })
|
||||||
async deleteUser(
|
async deleteUser(
|
||||||
@Arg('userId', () => Int) userId: number,
|
@Arg('userId', () => Int) userId: number,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<Date | null> {
|
): Promise<Date | null> {
|
||||||
const user = await dbUser.findOne({ id: userId })
|
const user = await dbUser.findOne({ id: userId })
|
||||||
// user exists ?
|
// user exists ?
|
||||||
@ -137,7 +139,7 @@ export class AdminResolver {
|
|||||||
throw new Error(`Could not find user with userId: ${userId}`)
|
throw new Error(`Could not find user with userId: ${userId}`)
|
||||||
}
|
}
|
||||||
// moderator user disabled own account?
|
// moderator user disabled own account?
|
||||||
const moderatorUser = context.user
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === userId) {
|
if (moderatorUser.id === userId) {
|
||||||
throw new Error('Moderator can not delete his own account!')
|
throw new Error('Moderator can not delete his own account!')
|
||||||
}
|
}
|
||||||
@ -151,11 +153,12 @@ export class AdminResolver {
|
|||||||
@Mutation(() => Date, { nullable: true })
|
@Mutation(() => Date, { nullable: true })
|
||||||
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
|
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
|
||||||
const user = await dbUser.findOne({ id: userId }, { withDeleted: true })
|
const user = await dbUser.findOne({ id: userId }, { withDeleted: true })
|
||||||
// user exists ?
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not find user with userId: ${userId}`)
|
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()
|
await user.recover()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -163,8 +166,9 @@ export class AdminResolver {
|
|||||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||||
@Mutation(() => [Number])
|
@Mutation(() => [Number])
|
||||||
async createPendingCreation(
|
async createPendingCreation(
|
||||||
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
|
@Args() { email, amount, memo, creationDate }: CreatePendingCreationArgs,
|
||||||
): Promise<number[]> {
|
@Ctx() context: Context,
|
||||||
|
): Promise<Decimal[]> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`Could not find user with email: ${email}`)
|
throw new Error(`Could not find user with email: ${email}`)
|
||||||
@ -175,16 +179,17 @@ export class AdminResolver {
|
|||||||
if (!user.emailChecked) {
|
if (!user.emailChecked) {
|
||||||
throw new Error('Creation could not be saved, Email is not activated')
|
throw new Error('Creation could not be saved, Email is not activated')
|
||||||
}
|
}
|
||||||
|
const moderator = getUser(context)
|
||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(user.id)
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
if (isCreationValid(creations, amount, creationDateObj)) {
|
if (isCreationValid(creations, amount, creationDateObj)) {
|
||||||
const adminPendingCreation = AdminPendingCreation.create()
|
const adminPendingCreation = AdminPendingCreation.create()
|
||||||
adminPendingCreation.userId = user.id
|
adminPendingCreation.userId = user.id
|
||||||
adminPendingCreation.amount = BigInt(amount)
|
adminPendingCreation.amount = amount
|
||||||
adminPendingCreation.created = new Date()
|
adminPendingCreation.created = new Date()
|
||||||
adminPendingCreation.date = creationDateObj
|
adminPendingCreation.date = creationDateObj
|
||||||
adminPendingCreation.memo = memo
|
adminPendingCreation.memo = memo
|
||||||
adminPendingCreation.moderator = moderator
|
adminPendingCreation.moderator = moderator.id
|
||||||
|
|
||||||
await AdminPendingCreation.save(adminPendingCreation)
|
await AdminPendingCreation.save(adminPendingCreation)
|
||||||
}
|
}
|
||||||
@ -196,12 +201,13 @@ export class AdminResolver {
|
|||||||
async createPendingCreations(
|
async createPendingCreations(
|
||||||
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
|
@Arg('pendingCreations', () => [CreatePendingCreationArgs])
|
||||||
pendingCreations: CreatePendingCreationArgs[],
|
pendingCreations: CreatePendingCreationArgs[],
|
||||||
|
@Ctx() context: Context,
|
||||||
): Promise<CreatePendingCreations> {
|
): Promise<CreatePendingCreations> {
|
||||||
let success = false
|
let success = false
|
||||||
const successfulCreation: string[] = []
|
const successfulCreation: string[] = []
|
||||||
const failedCreation: string[] = []
|
const failedCreation: string[] = []
|
||||||
for (const pendingCreation of pendingCreations) {
|
for (const pendingCreation of pendingCreations) {
|
||||||
await this.createPendingCreation(pendingCreation)
|
await this.createPendingCreation(pendingCreation, context)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
successfulCreation.push(pendingCreation.email)
|
successfulCreation.push(pendingCreation.email)
|
||||||
success = true
|
success = true
|
||||||
@ -220,7 +226,8 @@ export class AdminResolver {
|
|||||||
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
|
@Authorized([RIGHTS.UPDATE_PENDING_CREATION])
|
||||||
@Mutation(() => UpdatePendingCreation)
|
@Mutation(() => UpdatePendingCreation)
|
||||||
async updatePendingCreation(
|
async updatePendingCreation(
|
||||||
@Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs,
|
@Args() { id, email, amount, memo, creationDate }: UpdatePendingCreationArgs,
|
||||||
|
@Ctx() context: Context,
|
||||||
): Promise<UpdatePendingCreation> {
|
): Promise<UpdatePendingCreation> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -230,7 +237,13 @@ export class AdminResolver {
|
|||||||
throw new Error(`User was deleted (${email})`)
|
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) {
|
if (pendingCreationToUpdate.userId !== user.id) {
|
||||||
throw new Error('user of the pending creation and send user does not correspond')
|
throw new Error('user of the pending creation and send user does not correspond')
|
||||||
@ -242,20 +255,18 @@ export class AdminResolver {
|
|||||||
creations = updateCreations(creations, pendingCreationToUpdate)
|
creations = updateCreations(creations, pendingCreationToUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isCreationValid(creations, amount, creationDateObj)) {
|
// all possible cases not to be true are thrown in this function
|
||||||
throw new Error('Creation is not valid')
|
isCreationValid(creations, amount, creationDateObj)
|
||||||
}
|
pendingCreationToUpdate.amount = amount
|
||||||
pendingCreationToUpdate.amount = BigInt(amount)
|
|
||||||
pendingCreationToUpdate.memo = memo
|
pendingCreationToUpdate.memo = memo
|
||||||
pendingCreationToUpdate.date = new Date(creationDate)
|
pendingCreationToUpdate.date = new Date(creationDate)
|
||||||
pendingCreationToUpdate.moderator = moderator
|
pendingCreationToUpdate.moderator = moderator.id
|
||||||
|
|
||||||
await AdminPendingCreation.save(pendingCreationToUpdate)
|
await AdminPendingCreation.save(pendingCreationToUpdate)
|
||||||
const result = new UpdatePendingCreation()
|
const result = new UpdatePendingCreation()
|
||||||
result.amount = parseInt(amount.toString())
|
result.amount = amount
|
||||||
result.memo = pendingCreationToUpdate.memo
|
result.memo = pendingCreationToUpdate.memo
|
||||||
result.date = pendingCreationToUpdate.date
|
result.date = pendingCreationToUpdate.date
|
||||||
result.moderator = pendingCreationToUpdate.moderator
|
|
||||||
|
|
||||||
result.creation = await getUserCreation(user.id)
|
result.creation = await getUserCreation(user.id)
|
||||||
|
|
||||||
@ -280,7 +291,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...pendingCreation,
|
...pendingCreation,
|
||||||
amount: Number(pendingCreation.amount.toString()),
|
amount: pendingCreation.amount,
|
||||||
firstName: user ? user.firstName : '',
|
firstName: user ? user.firstName : '',
|
||||||
lastName: user ? user.lastName : '',
|
lastName: user ? user.lastName : '',
|
||||||
email: user ? user.email : '',
|
email: user ? user.email : '',
|
||||||
@ -292,8 +303,11 @@ export class AdminResolver {
|
|||||||
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
|
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
|
async deletePendingCreation(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||||
const entity = await AdminPendingCreation.findOneOrFail(id)
|
const pendingCreation = await AdminPendingCreation.findOne(id)
|
||||||
const res = await AdminPendingCreation.delete(entity)
|
if (!pendingCreation) {
|
||||||
|
throw new Error('Creation not found for given id.')
|
||||||
|
}
|
||||||
|
const res = await AdminPendingCreation.delete(pendingCreation)
|
||||||
return !!res
|
return !!res
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,10 +315,13 @@ export class AdminResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async confirmPendingCreation(
|
async confirmPendingCreation(
|
||||||
@Arg('id', () => Int) id: number,
|
@Arg('id', () => Int) id: number,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const pendingCreation = await AdminPendingCreation.findOneOrFail(id)
|
const pendingCreation = await AdminPendingCreation.findOne(id)
|
||||||
const moderatorUser = context.user
|
if (!pendingCreation) {
|
||||||
|
throw new Error('Creation not found to given id.')
|
||||||
|
}
|
||||||
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === pendingCreation.userId)
|
if (moderatorUser.id === pendingCreation.userId)
|
||||||
throw new Error('Moderator can not confirm own pending creation')
|
throw new Error('Moderator can not confirm own pending creation')
|
||||||
|
|
||||||
@ -312,7 +329,7 @@ export class AdminResolver {
|
|||||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
|
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
|
||||||
|
|
||||||
const creations = await getUserCreation(pendingCreation.userId, false)
|
const creations = await getUserCreation(pendingCreation.userId, false)
|
||||||
if (!isCreationValid(creations, Number(pendingCreation.amount), pendingCreation.date)) {
|
if (!isCreationValid(creations, pendingCreation.amount, pendingCreation.date)) {
|
||||||
throw new Error('Creation is not valid!!')
|
throw new Error('Creation is not valid!!')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,22 +344,22 @@ export class AdminResolver {
|
|||||||
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
|
decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, receivedCallDate)
|
||||||
newBalance = decay.balance
|
newBalance = decay.balance
|
||||||
}
|
}
|
||||||
// TODO pending creations decimal
|
newBalance = newBalance.add(pendingCreation.amount.toString())
|
||||||
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)).toString())
|
|
||||||
|
|
||||||
const transaction = new DbTransaction()
|
const transaction = new DbTransaction()
|
||||||
transaction.typeId = TransactionTypeId.CREATION
|
transaction.typeId = TransactionTypeId.CREATION
|
||||||
transaction.memo = pendingCreation.memo
|
transaction.memo = pendingCreation.memo
|
||||||
transaction.userId = pendingCreation.userId
|
transaction.userId = pendingCreation.userId
|
||||||
transaction.previous = lastTransaction ? lastTransaction.id : null
|
transaction.previous = lastTransaction ? lastTransaction.id : null
|
||||||
// TODO pending creations decimal
|
transaction.amount = pendingCreation.amount
|
||||||
transaction.amount = new Decimal(Number(pendingCreation.amount))
|
|
||||||
transaction.creationDate = pendingCreation.date
|
transaction.creationDate = pendingCreation.date
|
||||||
transaction.balance = newBalance
|
transaction.balance = newBalance
|
||||||
transaction.balanceDate = receivedCallDate
|
transaction.balanceDate = receivedCallDate
|
||||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||||
transaction.decayStart = decay ? decay.start : null
|
transaction.decayStart = decay ? decay.start : null
|
||||||
await transaction.save()
|
await transaction.save().catch(() => {
|
||||||
|
throw new Error('Unable to confirm creation.')
|
||||||
|
})
|
||||||
|
|
||||||
await AdminPendingCreation.delete(pendingCreation)
|
await AdminPendingCreation.delete(pendingCreation)
|
||||||
|
|
||||||
@ -369,14 +386,83 @@ export class AdminResolver {
|
|||||||
const user = await dbUser.findOneOrFail({ id: userId })
|
const user = await dbUser.findOneOrFail({ id: userId })
|
||||||
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
|
||||||
|
@Mutation(() => Boolean)
|
||||||
|
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||||
|
email = email.trim().toLowerCase()
|
||||||
|
const user = await dbUser.findOneOrFail({ email: email })
|
||||||
|
|
||||||
|
// can be both types: REGISTER and RESET_PASSWORD
|
||||||
|
let optInCode = await LoginEmailOptIn.findOne({
|
||||||
|
where: { userId: user.id },
|
||||||
|
order: { updatedAt: 'DESC' },
|
||||||
|
})
|
||||||
|
|
||||||
|
optInCode = await checkOptInCode(optInCode, user.id)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const emailSent = await sendAccountActivationEmail({
|
||||||
|
link: activationLink(optInCode),
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
email,
|
||||||
|
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||||
|
})
|
||||||
|
|
||||||
|
/* 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}`)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS_ADMIN])
|
||||||
|
@Query(() => TransactionLinkResult)
|
||||||
|
async listTransactionLinksAdmin(
|
||||||
|
@Args()
|
||||||
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||||
|
@Args()
|
||||||
|
filters: TransactionLinkFilters,
|
||||||
|
@Arg('userId', () => Int) userId: number,
|
||||||
|
): Promise<TransactionLinkResult> {
|
||||||
|
const user = await dbUser.findOneOrFail({ id: userId })
|
||||||
|
const where: {
|
||||||
|
userId: number
|
||||||
|
redeemedBy?: number | null
|
||||||
|
validUntil?: FindOperator<Date> | null
|
||||||
|
} = {
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
if (!filters.filterByRedeemed) where.redeemedBy = null
|
||||||
|
if (!filters.filterByExpired) where.validUntil = MoreThan(new Date())
|
||||||
|
const [transactionLinks, count] = await dbTransactionLink.findAndCount({
|
||||||
|
where,
|
||||||
|
withDeleted: filters.filterByDeleted,
|
||||||
|
order: {
|
||||||
|
createdAt: order,
|
||||||
|
},
|
||||||
|
skip: (currentPage - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
linkCount: count,
|
||||||
|
linkList: transactionLinks.map((tl) => new TransactionLink(tl, new User(user))),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreationMap {
|
interface CreationMap {
|
||||||
id: number
|
id: number
|
||||||
creations: number[]
|
creations: Decimal[]
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserCreation(id: number, includePending = true): Promise<number[]> {
|
async function getUserCreation(id: number, includePending = true): Promise<Decimal[]> {
|
||||||
const creations = await getUserCreations([id], includePending)
|
const creations = await getUserCreations([id], includePending)
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
}
|
}
|
||||||
@ -418,32 +504,32 @@ async function getUserCreations(ids: number[], includePending = true): Promise<C
|
|||||||
(raw: { month: string; id: string; creation: number[] }) =>
|
(raw: { month: string; id: string; creation: number[] }) =>
|
||||||
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
||||||
)
|
)
|
||||||
return MAX_CREATION_AMOUNT - (creation ? Number(creation.sum) : 0)
|
return MAX_CREATION_AMOUNT.minus(creation ? creation.sum : 0)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCreations(creations: number[], pendingCreation: AdminPendingCreation): number[] {
|
function updateCreations(creations: Decimal[], pendingCreation: AdminPendingCreation): Decimal[] {
|
||||||
const index = getCreationIndex(pendingCreation.date.getMonth())
|
const index = getCreationIndex(pendingCreation.date.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
throw new Error('You cannot create GDD for a month older than the last three months.')
|
||||||
}
|
}
|
||||||
creations[index] += parseInt(pendingCreation.amount.toString())
|
creations[index] = creations[index].plus(pendingCreation.amount.toString())
|
||||||
return creations
|
return creations
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCreationValid(creations: number[], amount: number, creationDate: Date) {
|
function isCreationValid(creations: Decimal[], amount: Decimal, creationDate: Date) {
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error(`No Creation found!`)
|
throw new Error('No information for available creations for the given date')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount > creations[index]) {
|
if (amount.greaterThan(creations[index].toString())) {
|
||||||
throw new Error(
|
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,40 +1,101 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
|
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
|
||||||
import { Balance } from '@model/Balance'
|
import { Balance } from '@model/Balance'
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { GdtResolver } from './GdtResolver'
|
||||||
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
import { getCustomRepository } from '@dbTools/typeorm'
|
||||||
|
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class BalanceResolver {
|
export class BalanceResolver {
|
||||||
@Authorized([RIGHTS.BALANCE])
|
@Authorized([RIGHTS.BALANCE])
|
||||||
@Query(() => Balance)
|
@Query(() => Balance)
|
||||||
async balance(@Ctx() context: any): Promise<Balance> {
|
async balance(@Ctx() context: Context): Promise<Balance> {
|
||||||
// load user and balance
|
const user = getUser(context)
|
||||||
const { user } = context
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
const lastTransaction = await Transaction.findOne(
|
logger.addContext('user', user.id)
|
||||||
{ userId: user.id },
|
logger.info(`balance(userId=${user.id})...`)
|
||||||
{ order: { balanceDate: 'DESC' } },
|
|
||||||
)
|
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
|
// No balance found
|
||||||
if (!lastTransaction) {
|
if (!lastTransaction) {
|
||||||
|
logger.info(`no balance found, return Default-Balance!`)
|
||||||
return new Balance({
|
return new Balance({
|
||||||
balance: new Decimal(0),
|
balance: new Decimal(0),
|
||||||
decay: new Decimal(0),
|
balanceGDT,
|
||||||
decay_date: now.toString(),
|
count: 0,
|
||||||
|
linkCount: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Balance({
|
const count =
|
||||||
balance: lastTransaction.balance,
|
context.transactionCount || context.transactionCount === 0
|
||||||
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
? context.transactionCount
|
||||||
decay_date: now.toString(),
|
: await dbTransaction.count({ where: { userId: user.id } })
|
||||||
|
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(
|
||||||
|
lastTransaction.balance,
|
||||||
|
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)
|
||||||
|
const { sumHoldAvailableAmount } = context.sumHoldAvailableAmount
|
||||||
|
? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
|
||||||
|
: await transactionLinkRepository.summary(user.id, now)
|
||||||
|
|
||||||
|
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,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import { Resolver, Query, Authorized } from 'type-graphql'
|
import { Resolver, Query, Authorized } from 'type-graphql'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { Context, getUser } from '@/server/context'
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
|
import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { GdtEntryList } from '@model/GdtEntryList'
|
import { GdtEntryList } from '@model/GdtEntryList'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
import { apiGet } from '@/apis/HttpRequest'
|
import { apiGet, apiPost } from '@/apis/HttpRequest'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
|
|
||||||
@ -13,14 +11,12 @@ import { RIGHTS } from '@/auth/RIGHTS'
|
|||||||
export class GdtResolver {
|
export class GdtResolver {
|
||||||
@Authorized([RIGHTS.LIST_GDT_ENTRIES])
|
@Authorized([RIGHTS.LIST_GDT_ENTRIES])
|
||||||
@Query(() => GdtEntryList)
|
@Query(() => GdtEntryList)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
async listGDTEntries(
|
async listGDTEntries(
|
||||||
@Args()
|
@Args()
|
||||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<GdtEntryList> {
|
): Promise<GdtEntryList> {
|
||||||
// load user
|
const userEntity = getUser(context)
|
||||||
const userEntity = context.user
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resultGDT = await apiGet(
|
const resultGDT = await apiGet(
|
||||||
@ -30,11 +26,30 @@ export class GdtResolver {
|
|||||||
throw new Error(resultGDT.data)
|
throw new Error(resultGDT.data)
|
||||||
}
|
}
|
||||||
return new GdtEntryList(resultGDT.data)
|
return new GdtEntryList(resultGDT.data)
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
throw new Error('GDT Server is not reachable.')
|
throw new Error('GDT Server is not reachable.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.GDT_BALANCE])
|
||||||
|
@Query(() => Number)
|
||||||
|
async gdtBalance(@Ctx() context: Context): Promise<number | null> {
|
||||||
|
const user = getUser(context)
|
||||||
|
try {
|
||||||
|
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||||
|
email: user.email,
|
||||||
|
})
|
||||||
|
if (!resultGDTSum.success) {
|
||||||
|
throw new Error('Call not successful')
|
||||||
|
}
|
||||||
|
return Number(resultGDTSum.data.sum) || 0
|
||||||
|
} catch (err) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Could not query GDT Server')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.EXIST_PID])
|
@Authorized([RIGHTS.EXIST_PID])
|
||||||
@Query(() => Number)
|
@Query(() => Number)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@ -1,6 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql'
|
import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql'
|
||||||
import {
|
import {
|
||||||
getKlickTippUser,
|
getKlickTippUser,
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { Context, getUser } from '@/server/context'
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
|
import { Resolver, Args, Arg, Authorized, Ctx, Mutation, Query, Int } from 'type-graphql'
|
||||||
import { TransactionLink } from '@model/TransactionLink'
|
import { TransactionLink } from '@model/TransactionLink'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
@ -38,9 +36,9 @@ export class TransactionLinkResolver {
|
|||||||
@Mutation(() => TransactionLink)
|
@Mutation(() => TransactionLink)
|
||||||
async createTransactionLink(
|
async createTransactionLink(
|
||||||
@Args() { amount, memo }: TransactionLinkArgs,
|
@Args() { amount, memo }: TransactionLinkArgs,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<TransactionLink> {
|
): Promise<TransactionLink> {
|
||||||
const { user } = context
|
const user = getUser(context)
|
||||||
|
|
||||||
const createdDate = new Date()
|
const createdDate = new Date()
|
||||||
const validUntil = transactionLinkExpireDate(createdDate)
|
const validUntil = transactionLinkExpireDate(createdDate)
|
||||||
@ -72,9 +70,9 @@ export class TransactionLinkResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async deleteTransactionLink(
|
async deleteTransactionLink(
|
||||||
@Arg('id', () => Int) id: number,
|
@Arg('id', () => Int) id: number,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const { user } = context
|
const user = getUser(context)
|
||||||
|
|
||||||
const transactionLink = await dbTransactionLink.findOne({ id })
|
const transactionLink = await dbTransactionLink.findOne({ id })
|
||||||
if (!transactionLink) {
|
if (!transactionLink) {
|
||||||
@ -113,9 +111,9 @@ export class TransactionLinkResolver {
|
|||||||
async listTransactionLinks(
|
async listTransactionLinks(
|
||||||
@Args()
|
@Args()
|
||||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<TransactionLink[]> {
|
): Promise<TransactionLink[]> {
|
||||||
const { user } = context
|
const user = getUser(context)
|
||||||
// const now = new Date()
|
// const now = new Date()
|
||||||
const transactionLinks = await dbTransactionLink.find({
|
const transactionLinks = await dbTransactionLink.find({
|
||||||
where: {
|
where: {
|
||||||
@ -136,9 +134,9 @@ export class TransactionLinkResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async redeemTransactionLink(
|
async redeemTransactionLink(
|
||||||
@Arg('code', () => String) code: string,
|
@Arg('code', () => String) code: string,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const { user } = context
|
const user = getUser(context)
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
/* eslint-disable new-cap */
|
/* eslint-disable new-cap */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
/* 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'
|
||||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
import CONFIG from '@/config'
|
|
||||||
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
||||||
|
|
||||||
import { Transaction } from '@model/Transaction'
|
import { Transaction } from '@model/Transaction'
|
||||||
@ -24,7 +25,6 @@ import { User as dbUser } from '@entity/User'
|
|||||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
import { apiPost } from '@/apis/HttpRequest'
|
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
import { calculateBalance, isHexPublicKey } from '@/util/validate'
|
import { calculateBalance, isHexPublicKey } from '@/util/validate'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
@ -32,7 +32,11 @@ import { User } from '@model/User'
|
|||||||
import { communityUser } from '@/util/communityUser'
|
import { communityUser } from '@/util/communityUser'
|
||||||
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
import { virtualLinkTransaction, virtualDecayTransaction } from '@/util/virtualTransactions'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { calculateDecay } from '@/util/decay'
|
|
||||||
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
|
|
||||||
|
const MEMO_MAX_CHARS = 255
|
||||||
|
const MEMO_MIN_CHARS = 5
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
@ -41,20 +45,43 @@ export const executeTransaction = async (
|
|||||||
recipient: dbUser,
|
recipient: dbUser,
|
||||||
transactionLink?: dbTransactionLink | null,
|
transactionLink?: dbTransactionLink | null,
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
|
logger.info(
|
||||||
|
`executeTransaction(amount=${amount}, memo=${memo}, sender=${sender}, recipient=${recipient})...`,
|
||||||
|
)
|
||||||
|
|
||||||
if (sender.id === recipient.id) {
|
if (sender.id === recipient.id) {
|
||||||
|
logger.error(`Sender and Recipient are the same.`)
|
||||||
throw new 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)`)
|
||||||
|
}
|
||||||
|
|
||||||
// validate amount
|
// validate amount
|
||||||
const receivedCallDate = new Date()
|
const receivedCallDate = new Date()
|
||||||
const sendBalance = await calculateBalance(sender.id, amount.mul(-1), receivedCallDate)
|
const sendBalance = await calculateBalance(
|
||||||
|
sender.id,
|
||||||
|
amount.mul(-1),
|
||||||
|
receivedCallDate,
|
||||||
|
transactionLink,
|
||||||
|
)
|
||||||
|
logger.debug(`calculated Balance=${sendBalance}`)
|
||||||
if (!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")
|
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||||
|
logger.debug(`open Transaction to write...`)
|
||||||
try {
|
try {
|
||||||
// transaction
|
// transaction
|
||||||
const transactionSend = new dbTransaction()
|
const transactionSend = new dbTransaction()
|
||||||
@ -71,6 +98,8 @@ export const executeTransaction = async (
|
|||||||
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
|
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||||
|
|
||||||
|
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
|
||||||
|
|
||||||
const transactionReceive = new dbTransaction()
|
const transactionReceive = new dbTransaction()
|
||||||
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
||||||
transactionReceive.memo = memo
|
transactionReceive.memo = memo
|
||||||
@ -86,12 +115,15 @@ export const executeTransaction = async (
|
|||||||
transactionReceive.linkedTransactionId = transactionSend.id
|
transactionReceive.linkedTransactionId = transactionSend.id
|
||||||
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
|
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||||
|
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
|
||||||
|
|
||||||
// Save linked transaction id for send
|
// Save linked transaction id for send
|
||||||
transactionSend.linkedTransactionId = transactionReceive.id
|
transactionSend.linkedTransactionId = transactionReceive.id
|
||||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||||
|
logger.debug(`send Transaction updated: ${transactionSend}`)
|
||||||
|
|
||||||
if (transactionLink) {
|
if (transactionLink) {
|
||||||
|
logger.info(`transactionLink: ${transactionLink}`)
|
||||||
transactionLink.redeemedAt = receivedCallDate
|
transactionLink.redeemedAt = receivedCallDate
|
||||||
transactionLink.redeemedBy = recipient.id
|
transactionLink.redeemedBy = recipient.id
|
||||||
await queryRunner.manager.update(
|
await queryRunner.manager.update(
|
||||||
@ -102,13 +134,15 @@ export const executeTransaction = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
|
logger.info(`commit Transaction successful...`)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
|
logger.error(`Transaction was not successful: ${e}`)
|
||||||
throw new Error(`Transaction was not successful: ${e}`)
|
throw new Error(`Transaction was not successful: ${e}`)
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
|
logger.debug(`prepare Email for transaction received...`)
|
||||||
// send notification email
|
// send notification email
|
||||||
// TODO: translate
|
// TODO: translate
|
||||||
await sendTransactionReceivedEmail({
|
await sendTransactionReceivedEmail({
|
||||||
@ -120,8 +154,9 @@ export const executeTransaction = async (
|
|||||||
senderEmail: sender.email,
|
senderEmail: sender.email,
|
||||||
amount,
|
amount,
|
||||||
memo,
|
memo,
|
||||||
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
|
logger.info(`finished executeTransaction successfully`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,34 +167,27 @@ export class TransactionResolver {
|
|||||||
async transactionList(
|
async transactionList(
|
||||||
@Args()
|
@Args()
|
||||||
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<TransactionList> {
|
): Promise<TransactionList> {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const user = context.user
|
const user = getUser(context)
|
||||||
|
|
||||||
|
logger.addContext('user', user.id)
|
||||||
|
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
|
||||||
|
|
||||||
// find current balance
|
// find current balance
|
||||||
const lastTransaction = await dbTransaction.findOne(
|
const lastTransaction = await dbTransaction.findOne(
|
||||||
{ userId: user.id },
|
{ userId: user.id },
|
||||||
{ order: { balanceDate: 'DESC' } },
|
{ order: { balanceDate: 'DESC' } },
|
||||||
)
|
)
|
||||||
|
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||||
|
|
||||||
// get GDT
|
const balanceResolver = new BalanceResolver()
|
||||||
let balanceGDT = null
|
context.lastTransaction = lastTransaction
|
||||||
try {
|
|
||||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
|
||||||
email: user.email,
|
|
||||||
})
|
|
||||||
if (!resultGDTSum.success) {
|
|
||||||
throw new Error('Call not successful')
|
|
||||||
}
|
|
||||||
balanceGDT = Number(resultGDTSum.data.sum) || 0
|
|
||||||
} catch (err: any) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Could not query GDT Server', err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastTransaction) {
|
if (!lastTransaction) {
|
||||||
return new TransactionList(new Decimal(0), [], 0, 0, balanceGDT)
|
logger.info('no lastTransaction')
|
||||||
|
return new TransactionList(await balanceResolver.balance(context), [])
|
||||||
}
|
}
|
||||||
|
|
||||||
// find transactions
|
// find transactions
|
||||||
@ -172,6 +200,7 @@ export class TransactionResolver {
|
|||||||
offset,
|
offset,
|
||||||
order,
|
order,
|
||||||
)
|
)
|
||||||
|
context.transactionCount = userTransactionsCount
|
||||||
|
|
||||||
// find involved users; I am involved
|
// find involved users; I am involved
|
||||||
const involvedUserIds: number[] = [user.id]
|
const involvedUserIds: number[] = [user.id]
|
||||||
@ -180,6 +209,8 @@ export class TransactionResolver {
|
|||||||
involvedUserIds.push(transaction.linkedUserId)
|
involvedUserIds.push(transaction.linkedUserId)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
logger.debug(`involvedUserIds=${involvedUserIds}`)
|
||||||
|
|
||||||
// We need to show the name for deleted users for old transactions
|
// We need to show the name for deleted users for old transactions
|
||||||
const involvedDbUsers = await dbUser
|
const involvedDbUsers = await dbUser
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -187,6 +218,7 @@ export class TransactionResolver {
|
|||||||
.where('id IN (:...userIds)', { userIds: involvedUserIds })
|
.where('id IN (:...userIds)', { userIds: involvedUserIds })
|
||||||
.getMany()
|
.getMany()
|
||||||
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
||||||
|
logger.debug(`involvedUsers=${involvedUsers}`)
|
||||||
|
|
||||||
const self = new User(user)
|
const self = new User(user)
|
||||||
const transactions: Transaction[] = []
|
const transactions: Transaction[] = []
|
||||||
@ -194,14 +226,30 @@ export class TransactionResolver {
|
|||||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||||
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
|
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
|
||||||
await transactionLinkRepository.summary(user.id, now)
|
await transactionLinkRepository.summary(user.id, now)
|
||||||
|
context.linkCount = transactionLinkcount
|
||||||
|
logger.debug(`transactionLinkcount=${transactionLinkcount}`)
|
||||||
|
context.sumHoldAvailableAmount = sumHoldAvailableAmount
|
||||||
|
logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
|
||||||
|
|
||||||
// decay & link transactions
|
// decay & link transactions
|
||||||
if (currentPage === 1 && order === Order.DESC) {
|
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(
|
transactions.push(
|
||||||
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
|
virtualDecayTransaction(
|
||||||
|
lastTransaction.balance,
|
||||||
|
lastTransaction.balanceDate,
|
||||||
|
now,
|
||||||
|
self,
|
||||||
|
sumHoldAvailableAmount,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
logger.debug(`transactions=${transactions}`)
|
||||||
|
|
||||||
// virtual transaction for pending transaction-links sum
|
// virtual transaction for pending transaction-links sum
|
||||||
if (sumHoldAvailableAmount.greaterThan(0)) {
|
if (sumHoldAvailableAmount.greaterThan(0)) {
|
||||||
|
logger.debug(`sumHoldAvailableAmount > 0: transactions=${transactions}`)
|
||||||
transactions.push(
|
transactions.push(
|
||||||
virtualLinkTransaction(
|
virtualLinkTransaction(
|
||||||
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
|
lastTransaction.balance.minus(sumHoldAvailableAmount.toString()),
|
||||||
@ -213,6 +261,7 @@ export class TransactionResolver {
|
|||||||
self,
|
self,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
logger.debug(`transactions=${transactions}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,45 +273,50 @@ export class TransactionResolver {
|
|||||||
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
|
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
|
||||||
transactions.push(new Transaction(userTransaction, self, linkedUser))
|
transactions.push(new Transaction(userTransaction, self, linkedUser))
|
||||||
})
|
})
|
||||||
|
logger.debug(`TransactionTypeId.CREATION: transactions=${transactions}`)
|
||||||
|
|
||||||
// Construct Result
|
// Construct Result
|
||||||
return new TransactionList(
|
return new TransactionList(await balanceResolver.balance(context), transactions)
|
||||||
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance.minus(
|
|
||||||
sumHoldAvailableAmount.toString(),
|
|
||||||
),
|
|
||||||
transactions,
|
|
||||||
userTransactionsCount,
|
|
||||||
transactionLinkcount,
|
|
||||||
balanceGDT,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.SEND_COINS])
|
@Authorized([RIGHTS.SEND_COINS])
|
||||||
@Mutation(() => String)
|
@Mutation(() => String)
|
||||||
async sendCoins(
|
async sendCoins(
|
||||||
@Args() { email, amount, memo }: TransactionSendArgs,
|
@Args() { email, amount, memo }: TransactionSendArgs,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
|
||||||
|
|
||||||
// TODO this is subject to replay attacks
|
// TODO this is subject to replay attacks
|
||||||
const senderUser = context.user
|
const senderUser = getUser(context)
|
||||||
if (senderUser.pubKey.length !== 32) {
|
if (senderUser.pubKey.length !== 32) {
|
||||||
|
logger.error(`invalid sender public key:${senderUser.pubKey}`)
|
||||||
throw new Error('invalid sender public key')
|
throw new Error('invalid sender public key')
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate recipient user
|
// validate recipient user
|
||||||
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
||||||
if (!recipientUser) {
|
if (!recipientUser) {
|
||||||
|
logger.error(`recipient not known: email=${email}`)
|
||||||
throw new Error('recipient not known')
|
throw new Error('recipient not known')
|
||||||
}
|
}
|
||||||
if (recipientUser.deletedAt) {
|
if (recipientUser.deletedAt) {
|
||||||
|
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account was deleted')
|
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'))) {
|
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
|
||||||
|
logger.error(`invalid recipient public key: recipientUser=${recipientUser}`)
|
||||||
throw new Error('invalid recipient public key')
|
throw new Error('invalid recipient public key')
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeTransaction(amount, memo, senderUser, recipientUser)
|
await executeTransaction(amount, memo, senderUser, recipientUser)
|
||||||
|
logger.info(
|
||||||
|
`successful executeTransaction(amount=${amount}, memo=${memo}, senderUser=${senderUser}, recipientUser=${recipientUser})`,
|
||||||
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers'
|
||||||
import { userFactory } from '@/seeds/factory/user'
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
||||||
import { login, logout } from '@/seeds/graphql/queries'
|
import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries'
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
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'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
|
|
||||||
@ -21,6 +25,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
jest.mock('@/mailer/sendResetPasswordEmail', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendResetPasswordEmail: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
jest.mock('@/apis/KlicktippController', () => {
|
jest.mock('@/apis/KlicktippController', () => {
|
||||||
return {
|
return {
|
||||||
@ -34,7 +45,7 @@ let mutate: any, query: any, con: any
|
|||||||
let testEnv: any
|
let testEnv: any
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
testEnv = await testEnvironment()
|
testEnv = await testEnvironment(logger)
|
||||||
mutate = testEnv.mutate
|
mutate = testEnv.mutate
|
||||||
query = testEnv.query
|
query = testEnv.query
|
||||||
con = testEnv.con
|
con = testEnv.con
|
||||||
@ -84,7 +95,7 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('filling all tables', () => {
|
describe('filling all tables', () => {
|
||||||
it('saves the user in login_user table', () => {
|
it('saves the user in users table', () => {
|
||||||
expect(user).toEqual([
|
expect(user).toEqual([
|
||||||
{
|
{
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
@ -99,6 +110,7 @@ describe('UserResolver', () => {
|
|||||||
emailChecked: false,
|
emailChecked: false,
|
||||||
passphrase: expect.any(String),
|
passphrase: expect.any(String),
|
||||||
language: 'de',
|
language: 'de',
|
||||||
|
isAdmin: null,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
publisherId: 1234,
|
publisherId: 1234,
|
||||||
referrerId: null,
|
referrerId: null,
|
||||||
@ -133,17 +145,20 @@ describe('UserResolver', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
duration: expect.any(String),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('email already exists', () => {
|
describe('email already exists', () => {
|
||||||
it('throws an error', async () => {
|
it('throws and logs an error', async () => {
|
||||||
await expect(mutate({ mutation: createUser, variables })).resolves.toEqual(
|
const mutation = await mutate({ mutation: createUser, variables })
|
||||||
|
expect(mutation).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('User already exists.')],
|
errors: [new GraphQLError('User already exists.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
expect(logger.error).toBeCalledWith('User already exists with this email=peter@lustig.de')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -220,10 +235,6 @@ describe('UserResolver', () => {
|
|||||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
expect(newUser[0].password).toEqual('3917921995996627700')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes the optin', async () => {
|
|
||||||
await expect(LoginEmailOptIn.find()).resolves.toHaveLength(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
it('calls the klicktipp API', () => {
|
it('calls the klicktipp API', () => {
|
||||||
expect(klicktippSignIn).toBeCalledWith(
|
expect(klicktippSignIn).toBeCalledWith(
|
||||||
@ -338,7 +349,7 @@ describe('UserResolver', () => {
|
|||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
hasElopage: false,
|
hasElopage: false,
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
isAdmin: false,
|
isAdmin: null,
|
||||||
klickTipp: {
|
klickTipp: {
|
||||||
newsletterState: false,
|
newsletterState: false,
|
||||||
},
|
},
|
||||||
@ -414,4 +425,368 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('verifyLogin', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
resetToken()
|
||||||
|
await expect(query({ query: verifyLogin })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user exists but is not logged in', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
resetToken()
|
||||||
|
await expect(query({ query: verifyLogin })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
const variables = {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
password: 'Aa12345_',
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await query({ query: login, variables })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns user object', async () => {
|
||||||
|
await expect(query({ query: verifyLogin })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
verifyLogin: {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
firstName: 'Bibi',
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
language: 'de',
|
||||||
|
coinanimation: true,
|
||||||
|
klickTipp: {
|
||||||
|
newsletterState: false,
|
||||||
|
},
|
||||||
|
hasElopage: false,
|
||||||
|
publisherId: 1234,
|
||||||
|
isAdmin: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('forgotPassword', () => {
|
||||||
|
const variables = { email: 'bibi@bloxberg.de' }
|
||||||
|
describe('user is not in DB', () => {
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
forgotPassword: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user exists in DB', () => {
|
||||||
|
let result: any
|
||||||
|
let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await resetEntity(LoginEmailOptIn)
|
||||||
|
result = await mutate({ mutation: forgotPassword, variables })
|
||||||
|
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
forgotPassword: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends reset password email', () => {
|
||||||
|
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||||
|
link: activationLink(loginEmailOptIn[0]),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
duration: expect.any(String),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('request reset password again', () => {
|
||||||
|
it('thows an error', async () => {
|
||||||
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('queryOptIn', () => {
|
||||||
|
let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('wrong optin code', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(
|
||||||
|
query({ query: queryOptIn, variables: { optIn: 'not-valid' } }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
// keep Whitspace in error message!
|
||||||
|
new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
|
||||||
|
"verificationCode": "not-valid"
|
||||||
|
}`),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('correct optin code', () => {
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: queryOptIn,
|
||||||
|
variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
queryOptIn: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('updateUserInfos', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
resetToken()
|
||||||
|
await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
password: 'Aa12345_',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
updateUserInfos: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('first-name, last-name and language', () => {
|
||||||
|
it('updates the fields in DB', async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: updateUserInfos,
|
||||||
|
variables: {
|
||||||
|
firstName: 'Benjamin',
|
||||||
|
lastName: 'Blümchen',
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(User.findOne()).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
firstName: 'Benjamin',
|
||||||
|
lastName: 'Blümchen',
|
||||||
|
language: 'en',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('language is not valid', () => {
|
||||||
|
it('thows an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: updateUserInfos,
|
||||||
|
variables: {
|
||||||
|
locale: 'not-valid',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError(`"not-valid" isn't a valid language`)],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('password', () => {
|
||||||
|
describe('wrong old password', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: updateUserInfos,
|
||||||
|
variables: {
|
||||||
|
password: 'wrong password',
|
||||||
|
passwordNew: 'Aa12345_',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('Old password is invalid')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('invalid new password', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: updateUserInfos,
|
||||||
|
variables: {
|
||||||
|
password: 'Aa12345_',
|
||||||
|
passwordNew: 'Aa12345',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError(
|
||||||
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('correct old and new password', () => {
|
||||||
|
it('returns true', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: updateUserInfos,
|
||||||
|
variables: {
|
||||||
|
password: 'Aa12345_',
|
||||||
|
passwordNew: 'Bb12345_',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { updateUserInfos: true },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can login wtih new password', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: login,
|
||||||
|
variables: {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
password: 'Bb12345_',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
login: expect.objectContaining({
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cannot login wtih old password', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: login,
|
||||||
|
variables: {
|
||||||
|
email: 'bibi@bloxberg.de',
|
||||||
|
password: 'Aa12345_',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('No user with this credentials')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('printTimeDuration', () => {
|
||||||
|
it('works with 10 minutes', () => {
|
||||||
|
expect(printTimeDuration(10)).toBe('10 minutes')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with 1440 minutes', () => {
|
||||||
|
expect(printTimeDuration(1440)).toBe('24 hours')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with 1410 minutes', () => {
|
||||||
|
expect(printTimeDuration(1410)).toBe('23 hours and 30 minutes')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
|
||||||
|
|
||||||
import fs from 'fs'
|
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 { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository } from '@dbTools/typeorm'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
@ -15,17 +15,13 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
|||||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||||
import { UserSettingRepository } from '@repository/UserSettingRepository'
|
import { UserSettingRepository } from '@repository/UserSettingRepository'
|
||||||
import { Setting } from '@enum/Setting'
|
import { Setting } from '@enum/Setting'
|
||||||
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { ROLE_ADMIN } from '@/auth/ROLES'
|
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { ServerUser } from '@entity/ServerUser'
|
|
||||||
|
|
||||||
const EMAIL_OPT_IN_RESET_PASSWORD = 2
|
|
||||||
const EMAIL_OPT_IN_REGISTER = 1
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const sodium = require('sodium-native')
|
const sodium = require('sodium-native')
|
||||||
@ -49,6 +45,7 @@ const WORDS = fs
|
|||||||
.toString()
|
.toString()
|
||||||
.split(',')
|
.split(',')
|
||||||
const PassphraseGenerate = (): string[] => {
|
const PassphraseGenerate = (): string[] => {
|
||||||
|
logger.trace('PassphraseGenerate...')
|
||||||
const result = []
|
const result = []
|
||||||
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
|
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
|
||||||
result.push(WORDS[sodium.randombytes_random() % 2048])
|
result.push(WORDS[sodium.randombytes_random() % 2048])
|
||||||
@ -57,7 +54,9 @@ const PassphraseGenerate = (): string[] => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
|
const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
|
||||||
|
logger.trace('KeyPairEd25519Create...')
|
||||||
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
|
if (!passphrase.length || passphrase.length < PHRASE_WORD_COUNT) {
|
||||||
|
logger.error('passphrase empty or to short')
|
||||||
throw new Error('passphrase empty or to short')
|
throw new Error('passphrase empty or to short')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,14 +84,19 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
|
|||||||
privKey,
|
privKey,
|
||||||
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
|
outputHashBuffer.slice(0, sodium.crypto_sign_SEEDBYTES),
|
||||||
)
|
)
|
||||||
|
logger.debug(`KeyPair creation ready. pubKey=${pubKey}`)
|
||||||
|
|
||||||
return [pubKey, privKey]
|
return [pubKey, privKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
|
const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
|
||||||
|
logger.trace('SecretKeyCryptographyCreateKey...')
|
||||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||||
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
|
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(
|
throw new Error(
|
||||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
||||||
)
|
)
|
||||||
@ -121,92 +125,104 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
|
|||||||
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
|
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
|
||||||
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`,
|
||||||
|
)
|
||||||
return [encryptionKeyHash, encryptionKey]
|
return [encryptionKeyHash, encryptionKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
const getEmailHash = (email: string): Buffer => {
|
const getEmailHash = (email: string): Buffer => {
|
||||||
|
logger.trace('getEmailHash...')
|
||||||
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
||||||
sodium.crypto_generichash(emailHash, Buffer.from(email))
|
sodium.crypto_generichash(emailHash, Buffer.from(email))
|
||||||
|
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
||||||
return emailHash
|
return emailHash
|
||||||
}
|
}
|
||||||
|
|
||||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
|
logger.trace('SecretKeyCryptographyEncrypt...')
|
||||||
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
|
const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES)
|
||||||
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
nonce.fill(31) // static nonce
|
nonce.fill(31) // static nonce
|
||||||
|
|
||||||
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
|
sodium.crypto_secretbox_easy(encrypted, message, nonce, encryptionKey)
|
||||||
|
logger.debug(`SecretKeyCryptographyEncrypt...successful: ${encrypted}`)
|
||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
|
const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
|
logger.trace('SecretKeyCryptographyDecrypt...')
|
||||||
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
|
const message = Buffer.alloc(encryptedMessage.length - sodium.crypto_secretbox_MACBYTES)
|
||||||
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
const nonce = Buffer.alloc(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
nonce.fill(31) // static nonce
|
nonce.fill(31) // static nonce
|
||||||
|
|
||||||
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
|
sodium.crypto_secretbox_open_easy(message, encryptedMessage, nonce, encryptionKey)
|
||||||
|
|
||||||
|
logger.debug(`SecretKeyCryptographyDecrypt...successful: ${message}`)
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
const createEmailOptIn = async (
|
|
||||||
loginUserId: number,
|
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||||
queryRunner: QueryRunner,
|
logger.trace('newEmailOptIn...')
|
||||||
): Promise<LoginEmailOptIn> => {
|
const emailOptIn = new LoginEmailOptIn()
|
||||||
let emailOptIn = await LoginEmailOptIn.findOne({
|
|
||||||
userId: loginUserId,
|
|
||||||
emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
|
|
||||||
})
|
|
||||||
if (emailOptIn) {
|
|
||||||
if (isOptInCodeValid(emailOptIn)) {
|
|
||||||
throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
|
||||||
}
|
|
||||||
emailOptIn.updatedAt = new Date()
|
|
||||||
emailOptIn.resendCount++
|
|
||||||
} else {
|
|
||||||
emailOptIn = new LoginEmailOptIn()
|
|
||||||
emailOptIn.verificationCode = random(64)
|
emailOptIn.verificationCode = random(64)
|
||||||
emailOptIn.userId = loginUserId
|
emailOptIn.userId = userId
|
||||||
emailOptIn.emailOptInTypeId = EMAIL_OPT_IN_REGISTER
|
emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||||
}
|
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Error while saving emailOptIn', error)
|
|
||||||
throw new Error('error saving email opt in')
|
|
||||||
})
|
|
||||||
return emailOptIn
|
return emailOptIn
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
|
// needed by AdminResolver
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
// checks if given code exists and can be resent
|
||||||
userId: loginUserId,
|
// if optIn does not exits, it is created
|
||||||
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
|
export const checkOptInCode = async (
|
||||||
})
|
optInCode: LoginEmailOptIn | undefined,
|
||||||
|
userId: number,
|
||||||
// Check for `CONFIG.EMAIL_CODE_VALID_TIME` minute delay
|
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
|
): Promise<LoginEmailOptIn> => {
|
||||||
|
logger.info(`checkOptInCode... ${optInCode}`)
|
||||||
if (optInCode) {
|
if (optInCode) {
|
||||||
if (isOptInCodeValid(optInCode)) {
|
if (!canResendOptIn(optInCode)) {
|
||||||
throw new Error(`email already sent less than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
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,
|
||||||
|
)} minutes ago`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
optInCode.updatedAt = new Date()
|
optInCode.updatedAt = new Date()
|
||||||
optInCode.resendCount++
|
optInCode.resendCount++
|
||||||
} else {
|
} else {
|
||||||
optInCode = new LoginEmailOptIn()
|
logger.trace('create new OptIn for userId=' + userId)
|
||||||
optInCode.verificationCode = random(64)
|
optInCode = newEmailOptIn(userId)
|
||||||
optInCode.userId = loginUserId
|
|
||||||
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
|
|
||||||
}
|
}
|
||||||
await LoginEmailOptIn.save(optInCode)
|
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
|
return optInCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
||||||
|
logger.debug(`activationLink(${LoginEmailOptIn})...`)
|
||||||
|
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
||||||
|
}
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class UserResolver {
|
export class UserResolver {
|
||||||
@Authorized([RIGHTS.VERIFY_LOGIN])
|
@Authorized([RIGHTS.VERIFY_LOGIN])
|
||||||
@Query(() => User)
|
@Query(() => User)
|
||||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||||
async verifyLogin(@Ctx() context: any): Promise<User> {
|
async verifyLogin(@Ctx() context: Context): Promise<User> {
|
||||||
|
logger.info('verifyLogin...')
|
||||||
// TODO refactor and do not have duplicate code with login(see below)
|
// TODO refactor and do not have duplicate code with login(see below)
|
||||||
const userEntity = context.user
|
const userEntity = getUser(context)
|
||||||
const user = new User(userEntity)
|
const user = new User(userEntity)
|
||||||
// user.pubkey = userEntity.pubKey.toString('hex')
|
// user.pubkey = userEntity.pubKey.toString('hex')
|
||||||
// Elopage Status & Stored PublisherId
|
// Elopage Status & Stored PublisherId
|
||||||
@ -217,11 +233,11 @@ export class UserResolver {
|
|||||||
const coinanimation = await userSettingRepository
|
const coinanimation = await userSettingRepository
|
||||||
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
|
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
logger.error('error:', error)
|
||||||
throw new Error(error)
|
throw new Error(error)
|
||||||
})
|
})
|
||||||
user.coinanimation = coinanimation
|
user.coinanimation = coinanimation
|
||||||
|
logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}, ${user.email}`)
|
||||||
user.isAdmin = context.role === ROLE_ADMIN
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,43 +246,50 @@ export class UserResolver {
|
|||||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||||
async login(
|
async login(
|
||||||
@Args() { email, password, publisherId }: UnsecureLoginArgs,
|
@Args() { email, password, publisherId }: UnsecureLoginArgs,
|
||||||
@Ctx() context: any,
|
@Ctx() context: Context,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
|
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
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')
|
throw new Error('No user with this credentials')
|
||||||
})
|
})
|
||||||
if (dbUser.deletedAt) {
|
if (dbUser.deletedAt) {
|
||||||
|
logger.error('The User was permanently deleted in database.')
|
||||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
throw new Error('This user was permanently deleted. Contact support for questions.')
|
||||||
}
|
}
|
||||||
if (!dbUser.emailChecked) {
|
if (!dbUser.emailChecked) {
|
||||||
|
logger.error('The Users email is not validate yet.')
|
||||||
throw new Error('User email not validated')
|
throw new Error('User email not validated')
|
||||||
}
|
}
|
||||||
if (dbUser.password === BigInt(0)) {
|
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
|
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||||
throw new Error('User has no password set yet')
|
throw new Error('User has no password set yet')
|
||||||
}
|
}
|
||||||
if (!dbUser.pubKey || !dbUser.privKey) {
|
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
|
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||||
throw new Error('User has no private or publicKey')
|
throw new Error('User has no private or publicKey')
|
||||||
}
|
}
|
||||||
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||||
const loginUserPassword = BigInt(dbUser.password.toString())
|
const loginUserPassword = BigInt(dbUser.password.toString())
|
||||||
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
|
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
|
||||||
|
logger.error('The User has no valid credentials.')
|
||||||
throw new Error('No user with this 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)
|
const user = new User(dbUser)
|
||||||
// user.email = email
|
logger.debug('user=' + user)
|
||||||
// user.pubkey = dbUser.pubKey.toString('hex')
|
|
||||||
user.language = dbUser.language
|
|
||||||
|
|
||||||
// Elopage Status & Stored PublisherId
|
// Elopage Status & Stored PublisherId
|
||||||
user.hasElopage = await this.hasElopage({ pubKey: dbUser.pubKey.toString('hex') })
|
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||||
|
logger.info('user.hasElopage=' + user.hasElopage)
|
||||||
if (!user.hasElopage && publisherId) {
|
if (!user.hasElopage && publisherId) {
|
||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
// TODO: Check if we can use updateUserInfos
|
|
||||||
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
|
|
||||||
dbUser.publisherId = publisherId
|
dbUser.publisherId = publisherId
|
||||||
DbUser.save(dbUser)
|
DbUser.save(dbUser)
|
||||||
}
|
}
|
||||||
@ -280,15 +303,11 @@ export class UserResolver {
|
|||||||
})
|
})
|
||||||
user.coinanimation = coinanimation
|
user.coinanimation = coinanimation
|
||||||
|
|
||||||
// context.role is not set to the actual role yet on login
|
|
||||||
const countServerUsers = await ServerUser.count({ email: user.email })
|
|
||||||
user.isAdmin = countServerUsers > 0
|
|
||||||
|
|
||||||
context.setHeaders.push({
|
context.setHeaders.push({
|
||||||
key: 'token',
|
key: 'token',
|
||||||
value: encode(dbUser.pubKey),
|
value: encode(dbUser.pubKey),
|
||||||
})
|
})
|
||||||
|
logger.info('successful Login:' + user)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,6 +319,9 @@ export class UserResolver {
|
|||||||
// The functionality is fully client side - the client just needs to delete his token with the current implementation.
|
// 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 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.
|
// 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,6 +331,9 @@ export class UserResolver {
|
|||||||
@Args()
|
@Args()
|
||||||
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
|
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
|
||||||
): Promise<User> {
|
): 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?
|
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||||
// default int publisher_id = 0;
|
// default int publisher_id = 0;
|
||||||
|
|
||||||
@ -321,7 +346,9 @@ export class UserResolver {
|
|||||||
email = email.trim().toLowerCase()
|
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
|
// 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 })
|
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||||
|
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||||
if (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.
|
// 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.`)
|
throw new Error(`User already exists.`)
|
||||||
}
|
}
|
||||||
@ -340,8 +367,10 @@ export class UserResolver {
|
|||||||
dbUser.language = language
|
dbUser.language = language
|
||||||
dbUser.publisherId = publisherId
|
dbUser.publisherId = publisherId
|
||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
|
logger.debug('new dbUser=' + dbUser)
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
const transactionLink = await dbTransactionLink.findOne({ code: redeemCode })
|
||||||
|
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||||
if (transactionLink) {
|
if (transactionLink) {
|
||||||
dbUser.referrerId = transactionLink.userId
|
dbUser.referrerId = transactionLink.userId
|
||||||
}
|
}
|
||||||
@ -358,14 +387,15 @@ export class UserResolver {
|
|||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
// eslint-disable-next-line no-console
|
logger.error('Error while saving dbUser', error)
|
||||||
console.log('Error while saving dbUser', error)
|
|
||||||
throw new Error('error saving user')
|
throw new Error('error saving user')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Store EmailOptIn in DB
|
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||||
// TODO: this has duplicate code with sendResetPasswordEmail
|
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||||
const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner)
|
logger.error('Error while saving emailOptIn', error)
|
||||||
|
throw new Error('error saving email opt in')
|
||||||
|
})
|
||||||
|
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
@ -378,99 +408,60 @@ export class UserResolver {
|
|||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
|
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||||
})
|
})
|
||||||
|
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
|
||||||
/* 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
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
// eslint-disable-next-line no-console
|
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||||
console.log(`Account confirmation link: ${activationLink}`)
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error(`error during create user with ${e}`)
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
|
logger.info('createUser() successful...')
|
||||||
return new User(dbUser)
|
return new User(dbUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
// THis is used by the admin only - should we move it to the admin resolver?
|
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
|
||||||
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
||||||
|
logger.info(`forgotPassword(${email})...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await DbUser.findOneOrFail({ email: email })
|
const user = await DbUser.findOne({ email })
|
||||||
|
if (!user) {
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
logger.warn(`no user found with ${email}`)
|
||||||
await queryRunner.connect()
|
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
|
||||||
|
|
||||||
try {
|
|
||||||
const emailOptIn = await createEmailOptIn(user.id, queryRunner)
|
|
||||||
|
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
|
||||||
/{optin}/g,
|
|
||||||
emailOptIn.verificationCode.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const emailSent = await sendAccountActivationEmail({
|
|
||||||
link: activationLink,
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
await queryRunner.commitTransaction()
|
|
||||||
} catch (e) {
|
|
||||||
await queryRunner.rollbackTransaction()
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
await queryRunner.release()
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
|
// can be both types: REGISTER and RESET_PASSWORD
|
||||||
@Query(() => Boolean)
|
let optInCode = await LoginEmailOptIn.findOne({
|
||||||
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
|
userId: user.id,
|
||||||
// TODO: this has duplicate code with createUser
|
})
|
||||||
email = email.trim().toLowerCase()
|
|
||||||
const user = await DbUser.findOneOrFail({ email })
|
|
||||||
|
|
||||||
const optInCode = await getOptInCode(user.id)
|
|
||||||
|
|
||||||
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
|
|
||||||
/{optin}/g,
|
|
||||||
optInCode.verificationCode.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendResetPasswordEmail({
|
const emailSent = await sendResetPasswordEmailMailer({
|
||||||
link,
|
link: activationLink(optInCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
|
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
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
// eslint-disable-next-line no-console
|
logger.debug(`Reset password link: ${activationLink(optInCode)}`)
|
||||||
console.log(`Reset password link: ${link}`)
|
|
||||||
}
|
}
|
||||||
*/
|
logger.info(`forgotPassword(${email}) successful...`)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -481,6 +472,7 @@ export class UserResolver {
|
|||||||
@Arg('code') code: string,
|
@Arg('code') code: string,
|
||||||
@Arg('password') password: string,
|
@Arg('password') password: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
logger.info(`setPassword(${code}, ***)...`)
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isPassword(password)) {
|
if (!isPassword(password)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -490,32 +482,44 @@ export class UserResolver {
|
|||||||
|
|
||||||
// Load code
|
// Load code
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||||
|
logger.error('Could not login with emailVerificationCode')
|
||||||
throw new 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
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInCodeValid(optInCode)) {
|
if (!isOptInValid(optInCode)) {
|
||||||
throw new Error(`email already more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
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
|
// load user
|
||||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
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')
|
throw new Error('Could not find corresponding Login User')
|
||||||
})
|
})
|
||||||
|
logger.debug('user with optInCode found...')
|
||||||
|
|
||||||
// Generate Passphrase if needed
|
// Generate Passphrase if needed
|
||||||
if (!user.passphrase) {
|
if (!user.passphrase) {
|
||||||
const passphrase = PassphraseGenerate()
|
const passphrase = PassphraseGenerate()
|
||||||
user.passphrase = passphrase.join(' ')
|
user.passphrase = passphrase.join(' ')
|
||||||
|
logger.debug('new Passphrase generated...')
|
||||||
}
|
}
|
||||||
|
|
||||||
const passphrase = user.passphrase.split(' ')
|
const passphrase = user.passphrase.split(' ')
|
||||||
if (passphrase.length < PHRASE_WORD_COUNT) {
|
if (passphrase.length < PHRASE_WORD_COUNT) {
|
||||||
|
logger.error('Could not load a correct passphrase')
|
||||||
// TODO if this can happen we cannot recover from that
|
// TODO if this can happen we cannot recover from that
|
||||||
// this seem to be good on production data, if we dont
|
// this seem to be good on production data, if we dont
|
||||||
// make a coding mistake we do not have a problem here
|
// make a coding mistake we do not have a problem here
|
||||||
throw new Error('Could not load a correct passphrase')
|
throw new Error('Could not load a correct passphrase')
|
||||||
}
|
}
|
||||||
|
logger.debug('Passphrase is valid...')
|
||||||
|
|
||||||
// Activate EMail
|
// Activate EMail
|
||||||
user.emailChecked = true
|
user.emailChecked = true
|
||||||
@ -527,6 +531,7 @@ export class UserResolver {
|
|||||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||||
user.pubKey = keyPair[0]
|
user.pubKey = keyPair[0]
|
||||||
user.privKey = encryptedPrivkey
|
user.privKey = encryptedPrivkey
|
||||||
|
logger.debug('User credentials updated ...')
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
@ -535,17 +540,15 @@ export class UserResolver {
|
|||||||
try {
|
try {
|
||||||
// Save user
|
// Save user
|
||||||
await queryRunner.manager.save(user).catch((error) => {
|
await queryRunner.manager.save(user).catch((error) => {
|
||||||
|
logger.error('error saving user: ' + error)
|
||||||
throw new Error('error saving user: ' + error)
|
throw new Error('error saving user: ' + error)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Delete Code
|
|
||||||
await queryRunner.manager.remove(optInCode).catch((error) => {
|
|
||||||
throw new Error('error deleting code: ' + error)
|
|
||||||
})
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
|
logger.info('User data written successfully...')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
|
logger.error('Error on writing User data:' + e)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
@ -553,10 +556,14 @@ export class UserResolver {
|
|||||||
|
|
||||||
// Sign into Klicktipp
|
// Sign into Klicktipp
|
||||||
// TODO do we always signUp the user? How to handle things with old users?
|
// TODO do we always signUp the user? How to handle things with old users?
|
||||||
if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) {
|
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||||
try {
|
try {
|
||||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
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?
|
// TODO is this a problem?
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
/* uncomment this, when you need the activation link on the console
|
/* uncomment this, when you need the activation link on the console
|
||||||
@ -571,11 +578,19 @@ export class UserResolver {
|
|||||||
@Authorized([RIGHTS.QUERY_OPT_IN])
|
@Authorized([RIGHTS.QUERY_OPT_IN])
|
||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||||
|
logger.info(`queryOptIn(${optIn})...`)
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
||||||
|
logger.debug(`found optInCode=${optInCode}`)
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInCodeValid(optInCode)) {
|
if (!isOptInValid(optInCode)) {
|
||||||
throw new Error(`email was sent more than $(CONFIG.EMAIL_CODE_VALID_TIME} minutes ago`)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -583,18 +598,13 @@ export class UserResolver {
|
|||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async updateUserInfos(
|
async updateUserInfos(
|
||||||
@Args()
|
@Args()
|
||||||
{
|
{ firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs,
|
||||||
firstName,
|
@Ctx() context: Context,
|
||||||
lastName,
|
|
||||||
language,
|
|
||||||
publisherId,
|
|
||||||
password,
|
|
||||||
passwordNew,
|
|
||||||
coinanimation,
|
|
||||||
}: UpdateUserInfosArgs,
|
|
||||||
@Ctx() context: any,
|
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const userEntity = context.user
|
logger.info(
|
||||||
|
`updateUserInfos(${firstName}, ${lastName}, ${language}, ***, ***, ${coinanimation})...`,
|
||||||
|
)
|
||||||
|
const userEntity = getUser(context)
|
||||||
|
|
||||||
if (firstName) {
|
if (firstName) {
|
||||||
userEntity.firstName = firstName
|
userEntity.firstName = firstName
|
||||||
@ -606,6 +616,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
if (language) {
|
if (language) {
|
||||||
if (!isLanguage(language)) {
|
if (!isLanguage(language)) {
|
||||||
|
logger.error(`"${language}" isn't a valid language`)
|
||||||
throw new Error(`"${language}" isn't a valid language`)
|
throw new Error(`"${language}" isn't a valid language`)
|
||||||
}
|
}
|
||||||
userEntity.language = language
|
userEntity.language = language
|
||||||
@ -614,6 +625,7 @@ export class UserResolver {
|
|||||||
if (password && passwordNew) {
|
if (password && passwordNew) {
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isPassword(passwordNew)) {
|
if (!isPassword(passwordNew)) {
|
||||||
|
logger.error('newPassword does not fullfil the rules')
|
||||||
throw new Error(
|
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!',
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
)
|
)
|
||||||
@ -622,24 +634,22 @@ export class UserResolver {
|
|||||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
||||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||||
|
logger.error(`Old password is invalid`)
|
||||||
throw new Error(`Old password is invalid`)
|
throw new Error(`Old password is invalid`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||||
|
logger.debug('oldPassword decrypted...')
|
||||||
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
||||||
|
logger.debug('newPasswordHash created...')
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||||
|
logger.debug('PrivateKey encrypted...')
|
||||||
|
|
||||||
// Save new password hash and newly encrypted private key
|
// Save new password hash and newly encrypted private key
|
||||||
userEntity.password = newPasswordHash[0].readBigUInt64LE()
|
userEntity.password = newPasswordHash[0].readBigUInt64LE()
|
||||||
userEntity.privKey = encryptedPrivkey
|
userEntity.privKey = encryptedPrivkey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save publisherId only if Elopage is not yet registered
|
|
||||||
if (publisherId && !(await this.hasElopage(context))) {
|
|
||||||
userEntity.publisherId = publisherId
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||||
@ -659,28 +669,60 @@ export class UserResolver {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
|
logger.debug('writing User data successful...')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
|
logger.error(`error on writing updated user data: ${e}`)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
|
logger.info('updateUserInfos() successfully finished...')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.HAS_ELOPAGE])
|
@Authorized([RIGHTS.HAS_ELOPAGE])
|
||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async hasElopage(@Ctx() context: any): Promise<boolean> {
|
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
||||||
|
logger.info(`hasElopage()...`)
|
||||||
const userEntity = context.user
|
const userEntity = context.user
|
||||||
if (!userEntity) {
|
if (!userEntity) {
|
||||||
|
logger.info('missing context.user for EloPage-check')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
const elopageBuys = hasElopageBuys(userEntity.email)
|
||||||
return hasElopageBuys(userEntity.email)
|
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||||
|
return elopageBuys
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function isOptInCodeValid(optInCode: LoginEmailOptIn) {
|
|
||||||
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
|
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||||
return timeElapsed <= CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000
|
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
||||||
|
// time is given in minutes
|
||||||
|
return timeElapsed <= duration * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||||
|
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||||
|
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||||
|
if (time > 60) {
|
||||||
|
return {
|
||||||
|
hours: Math.floor(time / 60),
|
||||||
|
minutes: time % 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { minutes: time }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const printTimeDuration = (duration: number): string => {
|
||||||
|
const time = getTimeDurationObject(duration)
|
||||||
|
const result = time.minutes > 0 ? `${time.minutes} minutes` : ''
|
||||||
|
if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '')
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ describe('sendAccountActivationEmail', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
duration: '23 hours and 30 minutes',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -23,7 +24,9 @@ describe('sendAccountActivationEmail', () => {
|
|||||||
to: `Peter Lustig <peter@lustig.de>`,
|
to: `Peter Lustig <peter@lustig.de>`,
|
||||||
subject: 'Gradido: E-Mail Überprüfung',
|
subject: 'Gradido: E-Mail Überprüfung',
|
||||||
text:
|
text:
|
||||||
expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('activationLink'),
|
expect.stringContaining('Hallo Peter Lustig') &&
|
||||||
|
expect.stringContaining('activationLink') &&
|
||||||
|
expect.stringContaining('23 Stunden und 30 Minuten'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { accountActivation } from './text/accountActivation'
|
import { accountActivation } from './text/accountActivation'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
export const sendAccountActivationEmail = (data: {
|
export const sendAccountActivationEmail = (data: {
|
||||||
link: string
|
link: string
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
|
duration: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return sendEMail({
|
return sendEMail({
|
||||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||||
subject: accountActivation.de.subject,
|
subject: accountActivation.de.subject,
|
||||||
text: accountActivation.de.text(data),
|
text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { sendEMail } from './sendEMail'
|
|||||||
import { createTransport } from 'nodemailer'
|
import { createTransport } from 'nodemailer'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
import { logger } from '@test/testSetup'
|
||||||
|
|
||||||
CONFIG.EMAIL = false
|
CONFIG.EMAIL = false
|
||||||
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
||||||
CONFIG.EMAIL_SMTP_PORT = '1234'
|
CONFIG.EMAIL_SMTP_PORT = '1234'
|
||||||
@ -26,11 +28,6 @@ jest.mock('nodemailer', () => {
|
|||||||
describe('sendEMail', () => {
|
describe('sendEMail', () => {
|
||||||
let result: boolean
|
let result: boolean
|
||||||
describe('config email is false', () => {
|
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 () => {
|
beforeEach(async () => {
|
||||||
result = await sendEMail({
|
result = await sendEMail({
|
||||||
to: 'receiver@mail.org',
|
to: 'receiver@mail.org',
|
||||||
@ -39,13 +36,8 @@ describe('sendEMail', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
it('logs warining', () => {
|
||||||
// eslint-disable-next-line no-console
|
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
|
||||||
console.log = consoleLog
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs warining to console', () => {
|
|
||||||
expect(consoleLogMock).toBeCalledWith('Emails are disabled via config')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns false', () => {
|
it('returns false', () => {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { createTransport } from 'nodemailer'
|
import { createTransport } from 'nodemailer'
|
||||||
|
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
@ -7,9 +8,10 @@ export const sendEMail = async (emailDef: {
|
|||||||
subject: string
|
subject: string
|
||||||
text: string
|
text: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
|
logger.info(`send Email: to=${emailDef.to}, subject=${emailDef.subject}, text=${emailDef.text}`)
|
||||||
|
|
||||||
if (!CONFIG.EMAIL) {
|
if (!CONFIG.EMAIL) {
|
||||||
// eslint-disable-next-line no-console
|
logger.info(`Emails are disabled via config...`)
|
||||||
console.log('Emails are disabled via config')
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const transporter = createTransport({
|
const transporter = createTransport({
|
||||||
@ -27,7 +29,9 @@ export const sendEMail = async (emailDef: {
|
|||||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
})
|
})
|
||||||
if (!info.messageId) {
|
if (!info.messageId) {
|
||||||
|
logger.error('error sending notification email, but transaction succeed')
|
||||||
throw new 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
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ describe('sendResetPasswordEmail', () => {
|
|||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
|
duration: '23 hours and 30 minutes',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -22,7 +23,10 @@ describe('sendResetPasswordEmail', () => {
|
|||||||
expect(sendEMail).toBeCalledWith({
|
expect(sendEMail).toBeCalledWith({
|
||||||
to: `Peter Lustig <peter@lustig.de>`,
|
to: `Peter Lustig <peter@lustig.de>`,
|
||||||
subject: 'Gradido: Passwort zurücksetzen',
|
subject: 'Gradido: Passwort zurücksetzen',
|
||||||
text: expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('resetLink'),
|
text:
|
||||||
|
expect.stringContaining('Hallo Peter Lustig') &&
|
||||||
|
expect.stringContaining('resetLink') &&
|
||||||
|
expect.stringContaining('23 Stunden und 30 Minuten'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { resetPassword } from './text/resetPassword'
|
import { resetPassword } from './text/resetPassword'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
export const sendResetPasswordEmail = (data: {
|
export const sendResetPasswordEmail = (data: {
|
||||||
link: string
|
link: string
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
|
duration: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
return sendEMail({
|
return sendEMail({
|
||||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||||
subject: resetPassword.de.subject,
|
subject: resetPassword.de.subject,
|
||||||
text: resetPassword.de.text(data),
|
text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ describe('sendTransactionReceivedEmail', () => {
|
|||||||
senderEmail: 'bibi@bloxberg.de',
|
senderEmail: 'bibi@bloxberg.de',
|
||||||
amount: new Decimal(42.0),
|
amount: new Decimal(42.0),
|
||||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||||
|
overviewURL: 'http://localhost/overview',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ describe('sendTransactionReceivedEmail', () => {
|
|||||||
expect.stringContaining('42,00 GDD') &&
|
expect.stringContaining('42,00 GDD') &&
|
||||||
expect.stringContaining('Bibi Bloxberg') &&
|
expect.stringContaining('Bibi Bloxberg') &&
|
||||||
expect.stringContaining('(bibi@bloxberg.de)') &&
|
expect.stringContaining('(bibi@bloxberg.de)') &&
|
||||||
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!'),
|
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') &&
|
||||||
|
expect.stringContaining('http://localhost/overview'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { sendEMail } from './sendEMail'
|
import { sendEMail } from './sendEMail'
|
||||||
import { transactionReceived } from './text/transactionReceived'
|
import { transactionReceived } from './text/transactionReceived'
|
||||||
@ -11,7 +12,14 @@ export const sendTransactionReceivedEmail = (data: {
|
|||||||
senderEmail: string
|
senderEmail: string
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
memo: string
|
memo: string
|
||||||
|
overviewURL: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
|
logger.info(
|
||||||
|
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||||
|
<${data.email}>,
|
||||||
|
subject=${transactionReceived.de.subject},
|
||||||
|
text=${transactionReceived.de.text(data)}`,
|
||||||
|
)
|
||||||
return sendEMail({
|
return sendEMail({
|
||||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||||
subject: transactionReceived.de.subject,
|
subject: transactionReceived.de.subject,
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
export const accountActivation = {
|
export const accountActivation = {
|
||||||
de: {
|
de: {
|
||||||
subject: 'Gradido: E-Mail Überprüfung',
|
subject: 'Gradido: E-Mail Überprüfung',
|
||||||
text: (data: { link: string; firstName: string; lastName: string; email: string }): string =>
|
text: (data: {
|
||||||
|
link: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
duration: string
|
||||||
|
resendLink: string
|
||||||
|
}): string =>
|
||||||
`Hallo ${data.firstName} ${data.lastName},
|
`Hallo ${data.firstName} ${data.lastName},
|
||||||
|
|
||||||
Deine E-Mail-Adresse wurde soeben bei Gradido registriert.
|
Deine E-Mail-Adresse wurde soeben bei Gradido registriert.
|
||||||
@ -10,6 +17,15 @@ Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradi
|
|||||||
${data.link}
|
${data.link}
|
||||||
oder kopiere den obigen Link in dein Browserfenster.
|
oder kopiere den obigen Link in dein Browserfenster.
|
||||||
|
|
||||||
|
Der Link hat eine Gültigkeit von ${data.duration
|
||||||
|
.replace('hours', 'Stunden')
|
||||||
|
.replace('minutes', 'Minuten')
|
||||||
|
.replace(
|
||||||
|
' and ',
|
||||||
|
' und ',
|
||||||
|
)}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:
|
||||||
|
${data.resendLink}
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team`,
|
dein Gradido-Team`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,13 +1,29 @@
|
|||||||
export const resetPassword = {
|
export const resetPassword = {
|
||||||
de: {
|
de: {
|
||||||
subject: 'Gradido: Passwort zurücksetzen',
|
subject: 'Gradido: Passwort zurücksetzen',
|
||||||
text: (data: { link: string; firstName: string; lastName: string; email: string }): string =>
|
text: (data: {
|
||||||
|
link: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
duration: string
|
||||||
|
resendLink: string
|
||||||
|
}): string =>
|
||||||
`Hallo ${data.firstName} ${data.lastName},
|
`Hallo ${data.firstName} ${data.lastName},
|
||||||
|
|
||||||
Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert.
|
Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert.
|
||||||
Wenn du es warst, klicke bitte auf den Link: ${data.link}
|
Wenn du es warst, klicke bitte auf den Link: ${data.link}
|
||||||
oder kopiere den obigen Link in Dein Browserfenster.
|
oder kopiere den obigen Link in Dein Browserfenster.
|
||||||
|
|
||||||
|
Der Link hat eine Gültigkeit von ${data.duration
|
||||||
|
.replace('hours', 'Stunden')
|
||||||
|
.replace('minutes', 'Minuten')
|
||||||
|
.replace(
|
||||||
|
' and ',
|
||||||
|
' und ',
|
||||||
|
)}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:
|
||||||
|
${data.resendLink}
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team`,
|
dein Gradido-Team`,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export const transactionReceived = {
|
|||||||
senderEmail: string
|
senderEmail: string
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
memo: string
|
memo: string
|
||||||
|
overviewURL: string
|
||||||
}): string =>
|
}): string =>
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||||
|
|
||||||
@ -25,6 +26,9 @@ ${data.memo}
|
|||||||
Bitte antworte nicht auf diese E-Mail!
|
Bitte antworte nicht auf diese E-Mail!
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team`,
|
dein Gradido-Team
|
||||||
|
|
||||||
|
|
||||||
|
Link zu deinem Konto: ${data.overviewURL}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,4 +4,6 @@ export interface CreationInterface {
|
|||||||
memo: string
|
memo: string
|
||||||
creationDate: string
|
creationDate: string
|
||||||
confirmed?: boolean
|
confirmed?: boolean
|
||||||
|
// number of months to move the confirmed creation to the past
|
||||||
|
moveCreationDate?: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,146 @@
|
|||||||
import { CreationInterface } from './CreationInterface'
|
import { CreationInterface } from './CreationInterface'
|
||||||
|
import { nMonthsBefore } from '../factory/creation'
|
||||||
|
|
||||||
const lastMonth = (date: Date): string => {
|
const bobsSendings = [
|
||||||
return new Date(date.getFullYear(), date.getMonth() - 1, 1).toISOString()
|
{
|
||||||
}
|
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[] = [
|
export const creations: CreationInterface[] = [
|
||||||
{
|
{
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
amount: 1000,
|
amount: 1000,
|
||||||
memo: 'Herzlich Willkommen bei Gradido!',
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
creationDate: lastMonth(new Date()),
|
creationDate: nMonthsBefore(new Date()),
|
||||||
confirmed: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email: 'bob@baumeister.de',
|
|
||||||
amount: 1000,
|
|
||||||
memo: 'Herzlich Willkommen bei Gradido!',
|
|
||||||
creationDate: lastMonth(new Date()),
|
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
|
moveCreationDate: 12,
|
||||||
},
|
},
|
||||||
|
...bobsTransactions,
|
||||||
{
|
{
|
||||||
email: 'raeuber@hotzenplotz.de',
|
email: 'raeuber@hotzenplotz.de',
|
||||||
amount: 1000,
|
amount: 1000,
|
||||||
memo: 'Herzlich Willkommen bei Gradido!',
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
creationDate: lastMonth(new Date()),
|
creationDate: nMonthsBefore(new Date()),
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@ -6,33 +6,51 @@ import { login } from '@/seeds/graphql/queries'
|
|||||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
|
import { Transaction } from '@entity/Transaction'
|
||||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||||
// import CONFIG from '@/config/index'
|
// import CONFIG from '@/config/index'
|
||||||
|
|
||||||
|
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||||
|
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
export const creationFactory = async (
|
export const creationFactory = async (
|
||||||
client: ApolloServerTestClient,
|
client: ApolloServerTestClient,
|
||||||
creation: CreationInterface,
|
creation: CreationInterface,
|
||||||
): Promise<void> => {
|
): Promise<AdminPendingCreation | void> => {
|
||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
|
||||||
// login as Peter Lustig (admin) and get his user ID
|
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||||
const {
|
|
||||||
data: {
|
|
||||||
login: { id },
|
|
||||||
},
|
|
||||||
} = 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 } })
|
const user = await User.findOneOrFail({ where: { email: creation.email } })
|
||||||
|
|
||||||
if (creation.confirmed) {
|
|
||||||
const pendingCreation = await AdminPendingCreation.findOneOrFail({
|
const pendingCreation = await AdminPendingCreation.findOneOrFail({
|
||||||
where: { userId: user.id },
|
where: { userId: user.id, amount: creation.amount },
|
||||||
order: { created: 'DESC' },
|
order: { created: 'DESC' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (creation.confirmed) {
|
||||||
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
|
await mutate({ mutation: confirmPendingCreation, variables: { id: pendingCreation.id } })
|
||||||
|
|
||||||
|
if (creation.moveCreationDate) {
|
||||||
|
const transaction = await Transaction.findOneOrFail({
|
||||||
|
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||||
|
order: { balanceDate: 'DESC' },
|
||||||
|
})
|
||||||
|
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||||
|
transaction.creationDate = new Date(
|
||||||
|
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||||
|
)
|
||||||
|
transaction.balanceDate = new Date(
|
||||||
|
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||||
|
)
|
||||||
|
await transaction.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return pendingCreation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||||
import { ServerUser } from '@entity/ServerUser'
|
|
||||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
export const userFactory = async (
|
export const userFactory = async (
|
||||||
client: ApolloServerTestClient,
|
client: ApolloServerTestClient,
|
||||||
user: UserInterface,
|
user: UserInterface,
|
||||||
): Promise<void> => {
|
): Promise<User> => {
|
||||||
const { mutate } = client
|
const { mutate } = client
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -25,27 +24,15 @@ export const userFactory = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
|
||||||
// get user from database
|
// get user from database
|
||||||
const dbUser = await User.findOneOrFail({ id })
|
const dbUser = await User.findOneOrFail({ id })
|
||||||
|
|
||||||
if (user.createdAt || user.deletedAt) {
|
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
||||||
if (user.createdAt) dbUser.createdAt = user.createdAt
|
if (user.createdAt) dbUser.createdAt = user.createdAt
|
||||||
if (user.deletedAt) dbUser.deletedAt = user.deletedAt
|
if (user.deletedAt) dbUser.deletedAt = user.deletedAt
|
||||||
|
if (user.isAdmin) dbUser.isAdmin = new Date()
|
||||||
await dbUser.save()
|
await dbUser.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.isAdmin) {
|
return dbUser
|
||||||
const admin = new ServerUser()
|
|
||||||
admin.username = dbUser.firstName
|
|
||||||
admin.password = 'please_refactor'
|
|
||||||
admin.email = dbUser.email
|
|
||||||
admin.role = 'admin'
|
|
||||||
admin.activated = 1
|
|
||||||
admin.lastLogin = new Date()
|
|
||||||
admin.created = dbUser.createdAt
|
|
||||||
admin.modified = dbUser.createdAt
|
|
||||||
await admin.save()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,12 @@ export const setPassword = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const forgotPassword = gql`
|
||||||
|
mutation ($email: String!) {
|
||||||
|
forgotPassword(email: $email)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const updateUserInfos = gql`
|
export const updateUserInfos = gql`
|
||||||
mutation (
|
mutation (
|
||||||
$firstName: String
|
$firstName: String
|
||||||
@ -78,20 +84,8 @@ export const createTransactionLink = gql`
|
|||||||
// from admin interface
|
// from admin interface
|
||||||
|
|
||||||
export const createPendingCreation = gql`
|
export const createPendingCreation = gql`
|
||||||
mutation (
|
mutation ($email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
|
||||||
$email: String!
|
createPendingCreation(email: $email, amount: $amount, memo: $memo, creationDate: $creationDate)
|
||||||
$amount: Float!
|
|
||||||
$memo: String!
|
|
||||||
$creationDate: String!
|
|
||||||
$moderator: Int!
|
|
||||||
) {
|
|
||||||
createPendingCreation(
|
|
||||||
email: $email
|
|
||||||
amount: $amount
|
|
||||||
memo: $memo
|
|
||||||
creationDate: $creationDate
|
|
||||||
moderator: $moderator
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@ -100,3 +94,77 @@ export const confirmPendingCreation = gql`
|
|||||||
confirmPendingCreation(id: $id)
|
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)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -43,6 +43,12 @@ export const logout = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const queryOptIn = gql`
|
||||||
|
query ($optIn: String!) {
|
||||||
|
queryOptIn(optIn: $optIn)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const transactionsQuery = gql`
|
export const transactionsQuery = gql`
|
||||||
query (
|
query (
|
||||||
$currentPage: Int = 1
|
$currentPage: Int = 1
|
||||||
@ -59,7 +65,6 @@ export const transactionsQuery = gql`
|
|||||||
balanceGDT
|
balanceGDT
|
||||||
count
|
count
|
||||||
balance
|
balance
|
||||||
decayStartBlock
|
|
||||||
transactions {
|
transactions {
|
||||||
id
|
id
|
||||||
typeId
|
typeId
|
||||||
@ -143,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 createServer from '../server/createServer'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
import { name, internet, random } from 'faker'
|
import { name, internet, datatype } from 'faker'
|
||||||
|
|
||||||
import { users } from './users/index'
|
import { users } from './users/index'
|
||||||
import { creations } from './creation/index'
|
import { creations } from './creation/index'
|
||||||
@ -13,6 +13,9 @@ import { userFactory } from './factory/user'
|
|||||||
import { creationFactory } from './factory/creation'
|
import { creationFactory } from './factory/creation'
|
||||||
import { transactionLinkFactory } from './factory/transactionLink'
|
import { transactionLinkFactory } from './factory/transactionLink'
|
||||||
import { entities } from '@entity/index'
|
import { entities } from '@entity/index'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
CONFIG.EMAIL = false
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
token: '',
|
token: '',
|
||||||
@ -26,7 +29,7 @@ const context = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const cleanDB = async () => {
|
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++) {
|
for (let i = 0; i < entities.length; i++) {
|
||||||
await resetEntity(entities[i])
|
await resetEntity(entities[i])
|
||||||
}
|
}
|
||||||
@ -57,13 +60,16 @@ const run = async () => {
|
|||||||
firstName: name.firstName(),
|
firstName: name.firstName(),
|
||||||
lastName: name.lastName(),
|
lastName: name.lastName(),
|
||||||
email: internet.email(),
|
email: internet.email(),
|
||||||
language: random.boolean() ? 'en' : 'de',
|
language: datatype.boolean() ? 'en' : 'de',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// create GDD
|
// create GDD
|
||||||
for (let i = 0; i < creations.length; i++) {
|
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])
|
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
|
// create Transaction Links
|
||||||
|
|||||||
@ -1,9 +1,24 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { Role } from '@/auth/Role'
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
import { User as dbUser } from '@entity/User'
|
||||||
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { ExpressContext } from 'apollo-server-express'
|
||||||
|
|
||||||
const context = (args: any) => {
|
export interface Context {
|
||||||
|
token: string | null
|
||||||
|
setHeaders: { key: string; value: string }[]
|
||||||
|
role?: Role
|
||||||
|
user?: dbUser
|
||||||
|
// hack to use less DB calls for Balance Resolver
|
||||||
|
lastTransaction?: dbTransaction
|
||||||
|
transactionCount?: number
|
||||||
|
linkCount?: number
|
||||||
|
sumHoldAvailableAmount?: Decimal
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = (args: ExpressContext): Context => {
|
||||||
const authorization = args.req.headers.authorization
|
const authorization = args.req.headers.authorization
|
||||||
let token = null
|
let token: string | null = null
|
||||||
if (authorization) {
|
if (authorization) {
|
||||||
token = authorization.replace(/^Bearer /, '')
|
token = authorization.replace(/^Bearer /, '')
|
||||||
}
|
}
|
||||||
@ -14,4 +29,9 @@ const context = (args: any) => {
|
|||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getUser = (context: Context): dbUser => {
|
||||||
|
if (context.user) return context.user
|
||||||
|
throw new Error('No user given in context!')
|
||||||
|
}
|
||||||
|
|
||||||
export default context
|
export default context
|
||||||
|
|||||||
@ -22,22 +22,32 @@ import schema from '@/graphql/schema'
|
|||||||
import { elopageWebhook } from '@/webhook/elopage'
|
import { elopageWebhook } from '@/webhook/elopage'
|
||||||
import { Connection } from '@dbTools/typeorm'
|
import { Connection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
|
import { apolloLogger } from './logger'
|
||||||
|
import { Logger } from 'log4js'
|
||||||
|
|
||||||
// TODO implement
|
// TODO implement
|
||||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||||
|
|
||||||
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
|
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const createServer = async (
|
||||||
const createServer = async (context: any = serverContext): Promise<ServerDef> => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
context: any = serverContext,
|
||||||
|
logger: Logger = apolloLogger,
|
||||||
|
): Promise<ServerDef> => {
|
||||||
|
logger.debug('createServer...')
|
||||||
|
|
||||||
// open mysql connection
|
// open mysql connection
|
||||||
const con = await connection()
|
const con = await connection()
|
||||||
if (!con || !con.isConnected) {
|
if (!con || !con.isConnected) {
|
||||||
|
logger.fatal(`Couldn't open connection to database!`)
|
||||||
throw new Error(`Fatal: Couldn't open connection to database`)
|
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for correct database version
|
// check for correct database version
|
||||||
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||||
if (!dbVersion) {
|
if (!dbVersion) {
|
||||||
|
logger.fatal('Fatal: Database Version incorrect')
|
||||||
throw new Error('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,
|
introspection: CONFIG.GRAPHIQL,
|
||||||
context,
|
context,
|
||||||
plugins,
|
plugins,
|
||||||
|
logger,
|
||||||
})
|
})
|
||||||
apollo.applyMiddleware({ app, path: '/' })
|
apollo.applyMiddleware({ app, path: '/' })
|
||||||
|
logger.debug('createServer...successful')
|
||||||
return { apollo, app, con }
|
return { apollo, app, con }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
backend/src/server/logger.ts
Normal file
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/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* 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 = {
|
const setHeadersPlugin = {
|
||||||
requestDidStart() {
|
requestDidStart() {
|
||||||
@ -22,24 +21,35 @@ const setHeadersPlugin = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const apolloLogPlugin = ApolloLogPlugin({
|
const filterVariables = (variables: any) => {
|
||||||
mutate: (data: LogMutateData) => {
|
const vars = clonedeep(variables)
|
||||||
// We need to deep clone the object in order to not modify the actual request
|
if (vars.password) vars.password = '***'
|
||||||
const dataCopy = cloneDeep(data)
|
if (vars.passwordNew) vars.passwordNew = '***'
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
// mask password if part of the query
|
const logPlugin = {
|
||||||
if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) {
|
requestDidStart(requestContext: any) {
|
||||||
dataCopy.context.request.variables.password = '***'
|
const { logger } = requestContext
|
||||||
}
|
const { query, mutation, variables } = requestContext.request
|
||||||
|
logger.trace(`Request:
|
||||||
// mask token at all times
|
${mutation || query}variables: ${JSON.stringify(filterVariables(variables), null, 2)}`)
|
||||||
dataCopy.context.context.token = '***'
|
return {
|
||||||
|
willSendResponse(requestContext: any) {
|
||||||
return dataCopy
|
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
|
||||||
},
|
},
|
||||||
})
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const plugins =
|
const plugins =
|
||||||
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, apolloLogPlugin]
|
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, logPlugin]
|
||||||
|
|
||||||
export default plugins
|
export default plugins
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { Migration } from '@entity/Migration'
|
import { Migration } from '@entity/Migration'
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
const getDBVersion = async (): Promise<string | null> => {
|
const getDBVersion = async (): Promise<string | null> => {
|
||||||
try {
|
try {
|
||||||
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
||||||
return dbVersion ? dbVersion.fileName : null
|
return dbVersion ? dbVersion.fileName : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
logger.error(error)
|
||||||
console.log(error)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,8 +14,7 @@ const getDBVersion = async (): Promise<string | null> => {
|
|||||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||||
const dbVersion = await getDBVersion()
|
const dbVersion = await getDBVersion()
|
||||||
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||||
// eslint-disable-next-line no-console
|
logger.error(
|
||||||
console.log(
|
|
||||||
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||||
dbVersion || 'None'
|
dbVersion || 'None'
|
||||||
}`,
|
}`,
|
||||||
|
|||||||
@ -20,6 +20,9 @@ const connection = async (): Promise<Connection | null> => {
|
|||||||
logger: new FileLogger('all', {
|
logger: new FileLogger('all', {
|
||||||
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||||
}),
|
}),
|
||||||
|
extra: {
|
||||||
|
charset: 'utf8mb4_unicode_ci',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const communityDbUser: dbUser = {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
emailChecked: false,
|
emailChecked: false,
|
||||||
language: '',
|
language: '',
|
||||||
|
isAdmin: null,
|
||||||
publisherId: 0,
|
publisherId: 0,
|
||||||
passphrase: '',
|
passphrase: '',
|
||||||
settings: [],
|
settings: [],
|
||||||
|
|||||||
@ -29,6 +29,7 @@ function calculateDecay(
|
|||||||
const decay: Decay = {
|
const decay: Decay = {
|
||||||
balance: amount,
|
balance: amount,
|
||||||
decay: new Decimal(0),
|
decay: new Decimal(0),
|
||||||
|
roundedDecay: new Decimal(0),
|
||||||
start: null,
|
start: null,
|
||||||
end: null,
|
end: null,
|
||||||
duration: null,
|
duration: null,
|
||||||
@ -52,6 +53,10 @@ function calculateDecay(
|
|||||||
decay.end = to
|
decay.end = to
|
||||||
decay.balance = decayFormula(amount, decay.duration)
|
decay.balance = decayFormula(amount, decay.duration)
|
||||||
decay.decay = decay.balance.minus(amount)
|
decay.decay = decay.balance.minus(amount)
|
||||||
|
decay.roundedDecay = amount
|
||||||
|
.toDecimalPlaces(2, Decimal.ROUND_DOWN)
|
||||||
|
.minus(decay.balance.toDecimalPlaces(2, Decimal.ROUND_DOWN).toString())
|
||||||
|
.mul(-1)
|
||||||
return decay
|
return decay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
5
backend/src/util/utilities.ts
Normal file
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]
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import { Transaction } from '@entity/Transaction'
|
|||||||
import { Decay } from '@model/Decay'
|
import { Decay } from '@model/Decay'
|
||||||
import { getCustomRepository } from '@dbTools/typeorm'
|
import { getCustomRepository } from '@dbTools/typeorm'
|
||||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||||
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
|
||||||
function isStringBoolean(value: string): boolean {
|
function isStringBoolean(value: string): boolean {
|
||||||
const lowerValue = value.toLowerCase()
|
const lowerValue = value.toLowerCase()
|
||||||
@ -21,6 +22,7 @@ async function calculateBalance(
|
|||||||
userId: number,
|
userId: number,
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
time: Date,
|
time: Date,
|
||||||
|
transactionLink?: dbTransactionLink | null,
|
||||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||||
if (!lastTransaction) return null
|
if (!lastTransaction) return null
|
||||||
@ -32,7 +34,13 @@ async function calculateBalance(
|
|||||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||||
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
|
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
|
||||||
|
|
||||||
if (balance.minus(sumHoldAvailableAmount.toString()).lessThan(0)) {
|
// If we want to redeem a link we need to make sure that the link amount is not considered as blocked
|
||||||
|
// else we cannot redeem links which are more or equal to half of what an account actually owns
|
||||||
|
const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0)
|
||||||
|
|
||||||
|
if (
|
||||||
|
balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0)
|
||||||
|
) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||||
|
|||||||
@ -42,11 +42,11 @@ const virtualLinkTransaction = (
|
|||||||
userId: -1,
|
userId: -1,
|
||||||
previous: -1,
|
previous: -1,
|
||||||
typeId: TransactionTypeId.LINK_SUMMARY,
|
typeId: TransactionTypeId.LINK_SUMMARY,
|
||||||
amount: amount,
|
amount: amount.toDecimalPlaces(2, Decimal.ROUND_FLOOR),
|
||||||
balance: balance,
|
balance: balance.toDecimalPlaces(2, Decimal.ROUND_DOWN),
|
||||||
balanceDate: validUntil,
|
balanceDate: validUntil,
|
||||||
decayStart: createdAt,
|
decayStart: createdAt,
|
||||||
decay: decay,
|
decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR),
|
||||||
memo: '',
|
memo: '',
|
||||||
creationDate: null,
|
creationDate: null,
|
||||||
...defaultModelFunctions,
|
...defaultModelFunctions,
|
||||||
@ -59,6 +59,7 @@ const virtualDecayTransaction = (
|
|||||||
balanceDate: Date,
|
balanceDate: Date,
|
||||||
time: Date = new Date(),
|
time: Date = new Date(),
|
||||||
user: User,
|
user: User,
|
||||||
|
holdAvailabeAmount: Decimal,
|
||||||
): Transaction => {
|
): Transaction => {
|
||||||
const decay = calculateDecay(balance, balanceDate, time)
|
const decay = calculateDecay(balance, balanceDate, time)
|
||||||
// const balance = decay.balance.minus(lastTransaction.balance)
|
// const balance = decay.balance.minus(lastTransaction.balance)
|
||||||
@ -67,10 +68,13 @@ const virtualDecayTransaction = (
|
|||||||
userId: -1,
|
userId: -1,
|
||||||
previous: -1,
|
previous: -1,
|
||||||
typeId: TransactionTypeId.DECAY,
|
typeId: TransactionTypeId.DECAY,
|
||||||
amount: decay.decay ? decay.decay : new Decimal(0), // new Decimal(0), // this kinda is wrong, but helps with the frontend query
|
amount: decay.decay ? decay.roundedDecay : new Decimal(0),
|
||||||
balance: decay.balance,
|
balance: decay.balance
|
||||||
|
.toDecimalPlaces(2, Decimal.ROUND_DOWN)
|
||||||
|
.minus(holdAvailabeAmount.toString())
|
||||||
|
.toDecimalPlaces(2, Decimal.ROUND_DOWN),
|
||||||
balanceDate: time,
|
balanceDate: time,
|
||||||
decay: decay.decay ? decay.decay : new Decimal(0),
|
decay: decay.decay ? decay.roundedDecay : new Decimal(0),
|
||||||
decayStart: decay.start,
|
decayStart: decay.start,
|
||||||
memo: '',
|
memo: '',
|
||||||
creationDate: null,
|
creationDate: null,
|
||||||
|
|||||||
@ -25,8 +25,8 @@ export const cleanDB = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const testEnvironment = async () => {
|
export const testEnvironment = async (logger?: any) => {
|
||||||
const server = await createServer(context)
|
const server = await createServer(context, logger)
|
||||||
const con = server.con
|
const con = server.con
|
||||||
const testClient = createTestClient(server.apollo)
|
const testClient = createTestClient(server.apollo)
|
||||||
const mutate = testClient.mutate
|
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.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
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@apollo/protobufjs@1.2.2", "@apollo/protobufjs@^1.0.3":
|
"@apollo/protobufjs@1.2.2":
|
||||||
version "1.2.2"
|
version "1.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c"
|
resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.2.tgz#4bd92cd7701ccaef6d517cdb75af2755f049f87c"
|
||||||
integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ==
|
integrity sha512-vF+zxhPiLtkwxONs6YanSt1EpwpGilThpneExUN5K3tCymuxNnVq2yojTvnpRjv2QfsEIt/n7ozPIIzBLwGIDQ==
|
||||||
@ -1265,24 +1265,6 @@ apollo-link@^1.2.14:
|
|||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
zen-observable-ts "^0.8.21"
|
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:
|
apollo-reporting-protobuf@^0.8.0:
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.8.0.tgz#ae9d967934d3d8ed816fc85a0d8068ef45c371b9"
|
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:
|
dependencies:
|
||||||
"@apollo/protobufjs" "1.2.2"
|
"@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:
|
apollo-server-caching@^0.7.0:
|
||||||
version "0.7.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.7.0.tgz#e6d1e68e3bb571cba63a61f60b434fb771c6ff39"
|
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"
|
subscriptions-transport-ws "^0.9.19"
|
||||||
uuid "^8.0.0"
|
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"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0"
|
resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-3.1.0.tgz#0733c2ef50aea596cc90cf40a53f6ea2ad402cd0"
|
||||||
integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==
|
integrity sha512-iGdZgEOAuVop3vb0F2J3+kaBVi4caMoxefHosxmgzAbbSpvWehB8Y1QiSyyMeouYC38XNVk5wnZl+jdGSsWsIQ==
|
||||||
@ -1371,13 +1346,6 @@ apollo-server-express@^2.25.2:
|
|||||||
subscriptions-transport-ws "^0.9.19"
|
subscriptions-transport-ws "^0.9.19"
|
||||||
type-is "^1.6.16"
|
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:
|
apollo-server-plugin-base@^0.13.0:
|
||||||
version "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"
|
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:
|
dependencies:
|
||||||
apollo-server-core "^2.25.2"
|
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:
|
apollo-server-types@^0.9.0:
|
||||||
version "0.9.0"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b"
|
resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.9.0.tgz#ccf550b33b07c48c72f104fbe2876232b404848b"
|
||||||
@ -1704,9 +1663,9 @@ camelcase@^6.2.0:
|
|||||||
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
|
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001264:
|
caniuse-lite@^1.0.30001264:
|
||||||
version "1.0.30001265"
|
version "1.0.30001325"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz#0613c9e6c922e422792e6fcefdf9a3afeee4f8c3"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz"
|
||||||
integrity sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==
|
integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
|
||||||
|
|
||||||
chalk@^2.0.0:
|
chalk@^2.0.0:
|
||||||
version "2.4.2"
|
version "2.4.2"
|
||||||
@ -1900,7 +1859,14 @@ create-require@^1.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||||
|
|
||||||
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
cross-env@^7.0.3:
|
||||||
|
version "7.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf"
|
||||||
|
integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==
|
||||||
|
dependencies:
|
||||||
|
cross-spawn "^7.0.1"
|
||||||
|
|
||||||
|
cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
version "7.0.3"
|
version "7.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
|
||||||
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
|
||||||
@ -1945,6 +1911,11 @@ data-urls@^2.0.0:
|
|||||||
whatwg-mimetype "^2.3.0"
|
whatwg-mimetype "^2.3.0"
|
||||||
whatwg-url "^8.0.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:
|
debug@2.6.9, debug@^2.2.0, debug@^2.6.9:
|
||||||
version "2.6.9"
|
version "2.6.9"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||||
@ -1966,6 +1937,13 @@ debug@^3.2.6, debug@^3.2.7:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms "^2.1.1"
|
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:
|
decimal.js-light@^2.5.1:
|
||||||
version "2.5.1"
|
version "2.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
|
||||||
@ -2551,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"
|
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
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:
|
fastq@^1.6.0:
|
||||||
version "1.13.0"
|
version "1.13.0"
|
||||||
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
|
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
|
||||||
@ -2625,6 +2598,11 @@ flatted@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
|
||||||
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
|
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:
|
follow-redirects@^1.14.0:
|
||||||
version "1.14.4"
|
version "1.14.4"
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
|
||||||
@ -2661,6 +2639,15 @@ fs-capacitor@^2.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
|
resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c"
|
||||||
integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA==
|
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:
|
fs.realpath@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
@ -2811,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"
|
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
|
||||||
integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
|
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:
|
graphql-extensions@^0.15.0:
|
||||||
version "0.15.0"
|
version "0.15.0"
|
||||||
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817"
|
resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.15.0.tgz#3f291f9274876b0c289fa4061909a12678bd9817"
|
||||||
@ -3803,6 +3795,15 @@ json5@^1.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.0"
|
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:
|
jsonwebtoken@^8.5.1:
|
||||||
version "8.5.1"
|
version "8.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
|
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
|
||||||
@ -3971,16 +3972,22 @@ lodash@4.x, lodash@^4.7.0:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
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:
|
loglevel@^1.6.7:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
|
||||||
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
|
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:
|
long@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
|
||||||
@ -4143,11 +4150,6 @@ named-placeholders@^1.1.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru-cache "^4.1.3"
|
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:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
@ -4739,6 +4741,11 @@ reusify@^1.0.4:
|
|||||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
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:
|
rimraf@^3.0.0, rimraf@^3.0.2:
|
||||||
version "3.0.2"
|
version "3.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||||
@ -4974,6 +4981,15 @@ stack-utils@^2.0.3:
|
|||||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||||
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
|
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:
|
streamsearch@0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
||||||
@ -5356,6 +5372,11 @@ universalify@^0.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
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:
|
unpipe@1.0.0, unpipe@~1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
|||||||
@ -5,4 +5,5 @@ module.exports = {
|
|||||||
trailingComma: "all",
|
trailingComma: "all",
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
bracketSpacing: true,
|
bracketSpacing: true,
|
||||||
|
endOfLine: "auto",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,23 +25,8 @@ yarn down
|
|||||||
yarn dev_down
|
yarn dev_down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reset database
|
||||||
## Reset DB
|
|
||||||
```
|
```
|
||||||
yarn dev_reset
|
yarn dev_reset
|
||||||
```
|
```
|
||||||
|
Runs all down migrations and after this all up migrations.
|
||||||
## Seed DB
|
|
||||||
```
|
|
||||||
yarn seed
|
|
||||||
```
|
|
||||||
|
|
||||||
## Seeded Users
|
|
||||||
|
|
||||||
| email | password | admin |
|
|
||||||
|------------------------|------------|---------|
|
|
||||||
| peter@lustig.de | `Aa12345_` | `true` |
|
|
||||||
| bibi@bloxberg.de | `Aa12345_` | `false` |
|
|
||||||
| raeuber@hotzenplotz.de | `Aa12345_` | `false` |
|
|
||||||
| bob@baumeister.de | `Aa12345_` | `false` |
|
|
||||||
|
|
||||||
|
|||||||
81
database/entity/0034-drop_server_user_table/User.ts
Normal file
81
database/entity/0034-drop_server_user_table/User.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
OneToMany,
|
||||||
|
DeleteDateColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { UserSetting } from '../UserSetting'
|
||||||
|
|
||||||
|
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||||
|
export class User extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
pubKey: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||||
|
privKey: Buffer
|
||||||
|
|
||||||
|
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'first_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
firstName: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'last_name',
|
||||||
|
length: 255,
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
lastName: string
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||||
|
password: BigInt
|
||||||
|
|
||||||
|
@Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
emailHash: Buffer
|
||||||
|
|
||||||
|
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||||
|
emailChecked: boolean
|
||||||
|
|
||||||
|
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||||
|
language: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||||
|
isAdmin: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||||
|
referrerId?: number | null
|
||||||
|
|
||||||
|
@Column({ name: 'publisher_id', default: 0 })
|
||||||
|
publisherId: number
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
name: 'passphrase',
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
nullable: true,
|
||||||
|
default: null,
|
||||||
|
})
|
||||||
|
passphrase: string
|
||||||
|
|
||||||
|
@OneToMany(() => UserSetting, (userSetting) => userSetting.user)
|
||||||
|
settings: UserSetting[]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user