mirror of
https://github.com/IT4Change/gradido.git
synced 2026-03-01 12:44:43 +00:00
Merge branch 'master' into fix_small_bugs
This commit is contained in:
commit
c48b7c45d9
15
.github/workflows/lint.yml
vendored
15
.github/workflows/lint.yml
vendored
@ -12,6 +12,7 @@ jobs:
|
||||
backend: ${{ steps.backend.outputs.success }}
|
||||
database: ${{ steps.database.outputs.success }}
|
||||
dht-node: ${{ steps.dht-node.outputs.success }}
|
||||
dlt-connector: ${{ steps.dlt-connector.outputs.success }}
|
||||
federation: ${{ steps.federation.outputs.success }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
@ -56,6 +57,12 @@ jobs:
|
||||
cd ./dht-node
|
||||
biome ci .
|
||||
echo "success=$([ $? -eq 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||
- name: Lint - DLT Connector
|
||||
id: dlt-connector
|
||||
run: |
|
||||
cd ./dlt-connector
|
||||
biome ci .
|
||||
echo "success=$([ $? -eq 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT
|
||||
- name: Lint - Federation
|
||||
id: federation
|
||||
run: |
|
||||
@ -111,6 +118,14 @@ jobs:
|
||||
- name: Check result from previous step
|
||||
run: if [ "${{ needs.lint.outputs.dht-node }}" != "true" ]; then exit 1; fi
|
||||
|
||||
lint_dlt_connector:
|
||||
name: Lint - DLT Connector
|
||||
needs: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check result from previous step
|
||||
run: if [ "${{ needs.lint.outputs.dlt-connector }}" != "true" ]; then exit 1; fi
|
||||
|
||||
lint_federation:
|
||||
name: Lint - Federation
|
||||
needs: lint
|
||||
|
||||
19
.github/workflows/test_backend.yml
vendored
19
.github/workflows/test_backend.yml
vendored
@ -51,8 +51,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: docker-compose mariadb redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@ -93,18 +93,3 @@ jobs:
|
||||
|
||||
- name: Backend | Typecheck
|
||||
run: turbo backend#typecheck backend#build
|
||||
|
||||
locales:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
name: Locales - Backend
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Backend | Locales
|
||||
run: cd backend && bun locales
|
||||
15
.github/workflows/test_core.yml
vendored
15
.github/workflows/test_core.yml
vendored
@ -43,3 +43,18 @@ jobs:
|
||||
- name: typecheck && unit test
|
||||
run: turbo core#test core#typecheck
|
||||
|
||||
locales:
|
||||
if: needs.files-changed.outputs.core == 'true'
|
||||
name: Locales - Core
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Core | Locales
|
||||
run: cd core && bun locales
|
||||
|
||||
|
||||
2
.github/workflows/test_database.yml
vendored
2
.github/workflows/test_database.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
node-version: '18.20.7'
|
||||
|
||||
- name: Database | docker-compose
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
4
.github/workflows/test_dht_node.yml
vendored
4
.github/workflows/test_dht_node.yml
vendored
@ -48,8 +48,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: docker-compose mariadb redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
41
.github/workflows/test_dlt_connector.yml
vendored
41
.github/workflows/test_dlt_connector.yml
vendored
@ -29,16 +29,19 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build 'test' image
|
||||
- name: create .env
|
||||
run: |
|
||||
docker build --target test -t "gradido/dlt-connector:test" -f dlt-connector/Dockerfile .
|
||||
docker save "gradido/dlt-connector:test" > /tmp/dlt-connector.tar
|
||||
cd dlt-connector
|
||||
cat <<EOF > .env
|
||||
GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=$(openssl rand -hex 16)
|
||||
GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=$(openssl rand -hex 16)
|
||||
HOME_COMMUNITY_SEED=$(openssl rand -hex 32)
|
||||
HIERO_OPERATOR_KEY=$(openssl rand -hex 32)
|
||||
HIERO_OPERATOR_ID="0.0.2"
|
||||
EOF
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-dlt-connector-test
|
||||
path: /tmp/dlt-connector.tar
|
||||
- name: Build 'test' image
|
||||
run: docker build --target production -t "gradido/dlt-connector:productionTest" -f dlt-connector/Dockerfile .
|
||||
|
||||
unit_test:
|
||||
name: Unit Tests - DLT Connector
|
||||
@ -49,12 +52,20 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: DLT-Connector | docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
- name: install dependencies
|
||||
run: bun install --filter shared --frozen-lockfile && cd dlt-connector && bun install --frozen-lockfile
|
||||
|
||||
- name: typecheck && unit test
|
||||
run: |
|
||||
export GRADIDO_BLOCKCHAIN_CRYPTO_APP_SECRET=$(openssl rand -hex 16)
|
||||
export GRADIDO_BLOCKCHAIN_SERVER_CRYPTO_KEY=$(openssl rand -hex 16)
|
||||
export HOME_COMMUNITY_SEED=$(openssl rand -hex 32)
|
||||
export HIERO_OPERATOR_KEY=$(openssl rand -hex 32)
|
||||
export HIERO_OPERATOR_ID="0.0.2"
|
||||
cd dlt-connector && bun typecheck && bun test
|
||||
|
||||
- name: DLT-Connector | Unit tests
|
||||
run: cd dlt-database && yarn && yarn build && cd ../dlt-connector && yarn && yarn test
|
||||
|
||||
21
.github/workflows/test_e2e.yml
vendored
21
.github/workflows/test_e2e.yml
vendored
@ -20,8 +20,8 @@ jobs:
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
- name: Boot up test system | docker-compose mariadb mailserver redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver redis
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
@ -63,10 +63,11 @@ jobs:
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
- name: wait for nginx, mailserver and redis to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
|
||||
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
|
||||
until nc -z 127.0.0.1 6379; do echo waiting for redis; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
@ -125,8 +126,8 @@ jobs:
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
- name: Boot up test system | docker-compose mariadb mailserver redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver redis
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
@ -169,10 +170,11 @@ jobs:
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
- name: wait for nginx, mailserver and redis to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
|
||||
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
|
||||
until nc -z 127.0.0.1 6379; do echo waiting for redis; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
@ -210,8 +212,8 @@ jobs:
|
||||
with:
|
||||
bun-version-file: '.bun-version'
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
- name: Boot up test system | docker-compose mariadb mailserver redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver redis
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
@ -250,10 +252,11 @@ jobs:
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
- name: wait for nginx, mailserver and redis to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
|
||||
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
|
||||
until nc -z 127.0.0.1 6379; do echo waiting for redis; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
|
||||
4
.github/workflows/test_federation.yml
vendored
4
.github/workflows/test_federation.yml
vendored
@ -48,8 +48,8 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: docker-compose mariadb redis
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb redis
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1 +1,4 @@
|
||||
|
||||
[submodule "inspector"]
|
||||
path = inspector
|
||||
url = https://github.com/gradido/inspector.git
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@ -4,9 +4,35 @@ 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).
|
||||
|
||||
#### [v2.7.0](https://github.com/gradido/gradido/compare/v2.7.0...v2.7.0)
|
||||
#### [v2.7.3](https://github.com/gradido/gradido/compare/v2.7.2...v2.7.3)
|
||||
|
||||
- fixes [`414ff8a`](https://github.com/gradido/gradido/commit/414ff8ac5a7477109f80123ccca5c4c8ed4511b2)
|
||||
- feat(admin): show user registered at in admin [`#3589`](https://github.com/gradido/gradido/pull/3589)
|
||||
- feat(backend): 3573 feature introduce distributed semaphore base on redis [`#3580`](https://github.com/gradido/gradido/pull/3580)
|
||||
- fix(workflow): make deployment install script more robust [`#3588`](https://github.com/gradido/gradido/pull/3588)
|
||||
- chore(release): v2.7.2 [`#3587`](https://github.com/gradido/gradido/pull/3587)
|
||||
|
||||
#### [v2.7.2](https://github.com/gradido/gradido/compare/v2.7.0...v2.7.2)
|
||||
|
||||
> 2 December 2025
|
||||
|
||||
- feat(frontend): success message on create contribution like on send [`#3583`](https://github.com/gradido/gradido/pull/3583)
|
||||
- refactor(backend): rewrite seeding in database [`#3586`](https://github.com/gradido/gradido/pull/3586)
|
||||
- fix(other): complete email tests and move localization complete into core [`#3585`](https://github.com/gradido/gradido/pull/3585)
|
||||
- fix(workflow): editor warnings [`#3584`](https://github.com/gradido/gradido/pull/3584)
|
||||
- fix(other): fix code which lead to biome linting errors [`#3582`](https://github.com/gradido/gradido/pull/3582)
|
||||
- refactor(backend): moved email to core [`#3579`](https://github.com/gradido/gradido/pull/3579)
|
||||
- refactor(database): stabilize entity loading across runtimes by introducing deferred relation resolution [`#3578`](https://github.com/gradido/gradido/pull/3578)
|
||||
- chore(release): v2.7.1 [`#3577`](https://github.com/gradido/gradido/pull/3577)
|
||||
- feat(frontend): new startpage images [`#3576`](https://github.com/gradido/gradido/pull/3576)
|
||||
- feat(frontend): update login subtitle [`#3574`](https://github.com/gradido/gradido/pull/3574)
|
||||
- feat(frontend): update copy symbol and change link order [`#3575`](https://github.com/gradido/gradido/pull/3575)
|
||||
- fix(backend): correct seeding [`#3572`](https://github.com/gradido/gradido/pull/3572)
|
||||
- feat(dlt): dlt-connector takes care of gradido node [`#3568`](https://github.com/gradido/gradido/pull/3568)
|
||||
- refactor(dlt): upgrade dlt for using gradido blockchain lib and hiero [`#3551`](https://github.com/gradido/gradido/pull/3551)
|
||||
- chore(release): v2.7.0 [`#3557`](https://github.com/gradido/gradido/pull/3557)
|
||||
- feat(federation): use own table for handshake state [`#3555`](https://github.com/gradido/gradido/pull/3555)
|
||||
- fix(workflow): use bun instead of yarn [`#3550`](https://github.com/gradido/gradido/pull/3550)
|
||||
- feat(workflow): adjust for new bun version [`#3554`](https://github.com/gradido/gradido/pull/3554)
|
||||
|
||||
#### [v2.7.0](https://github.com/gradido/gradido/compare/2.6.1...v2.7.0)
|
||||
|
||||
|
||||
31
README.md
31
README.md
@ -20,6 +20,7 @@ Clone the Gradido repository to your local machine.
|
||||
```bash
|
||||
git clone https://github.com/gradido/gradido.git
|
||||
cd gradido
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
For local development, you can run Gradido with **Docker** or **natively**, depending on your preferences and system setup. If you don't have a native MariaDB or MySQL installation, Docker can be used to handle the database as well.
|
||||
@ -105,6 +106,22 @@ turbo start
|
||||
|
||||
[More Infos for using turbo](./working-native.md)
|
||||
|
||||
### Dependencies & Bundling
|
||||
This project uses esbuild to bundle the main modules (backend, dht-node, federation) into single JavaScript files for optimized deployment. To ensure a minimal and reliable Docker image, dependencies are intentionally split:
|
||||
|
||||
- dependencies: Only packages that cannot be bundled by esbuild into the output files.
|
||||
Examples include:
|
||||
- Native modules (sodium-native)
|
||||
- Packages incompatible with bundling (email-templates)
|
||||
- Runtime helpers (cross-env)
|
||||
- devDependencies: All other packages that are fully bundled into the build output by esbuild.
|
||||
|
||||
This setup ensures that:
|
||||
- The production Docker image contains only the minimal set of necessary runtime modules.
|
||||
- Native or runtime-sensitive packages are included in node_modules for proper execution.
|
||||
|
||||
Note: Even if Docker is not used in all environments, this organization ensures consistent and predictable builds across different platforms.
|
||||
|
||||
|
||||
### For Windows
|
||||
|
||||
@ -189,11 +206,11 @@ describe('test', () => {
|
||||
```ts
|
||||
import { clearLogs, printLogs } from 'config-schema/test/testSetup'
|
||||
```
|
||||
- vitest (frontend, admin, database):
|
||||
- vitest (frontend, admin):
|
||||
```ts
|
||||
import { clearLogs, printLogs } from 'config-schema/test/testSetup.vitest'
|
||||
```
|
||||
- bun (shared, core):
|
||||
- bun (shared, core, database):
|
||||
```ts
|
||||
import { clearLogs, printLogs } from 'config-schema/test/testSetup.bun'
|
||||
```
|
||||
@ -211,6 +228,16 @@ In root folder calling `bun clear` will clear all turbo caches, node_modules and
|
||||
bun clear
|
||||
```
|
||||
|
||||
### git Submodule
|
||||
The new Module `inspector` was added as git submodule.
|
||||
So after
|
||||
- `git clone`
|
||||
- `git checkout`
|
||||
- `git pull`
|
||||
|
||||
you have to run `git submodule update --init` to get the correct submodule version.
|
||||
|
||||
[Read More](https://git-scm.com/book/en/v2/Git-Tools-Submodules)
|
||||
|
||||
## Services defined in this package
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administration Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Gradido Academy - https://www.gradido.net",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.3",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -16,6 +16,10 @@
|
||||
<div v-html="data.value" />
|
||||
</template>
|
||||
|
||||
<template #cell(createdAt)="data">
|
||||
{{ $d(new Date(data.value), 'long') }}
|
||||
</template>
|
||||
|
||||
<template #cell(status)="row">
|
||||
<div class="d-flex gap-3 justify-content-end align-items-center">
|
||||
<div
|
||||
|
||||
@ -26,6 +26,7 @@ export const searchUsers = gql`
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
createdAt
|
||||
roles
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,6 +244,7 @@
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"registered": "Registriert",
|
||||
"registered_at": "Registriert am",
|
||||
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
|
||||
"reset": "Zurücksetzen",
|
||||
"save": "Speichern",
|
||||
|
||||
@ -244,6 +244,7 @@
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"registered": "Registered",
|
||||
"registered_at": "Registered at",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"reset": "Reset",
|
||||
"save": "Save",
|
||||
|
||||
@ -125,6 +125,7 @@ const fields = computed(() => [
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
{ key: 'createdAt', label: t('registered_at') },
|
||||
// { key: 'show_details', label: t('details') },
|
||||
// { key: 'confirm_mail', label: t('confirmed') },
|
||||
// { key: 'has_elopage', label: 'elopage' },
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Server
|
||||
PORT=4000
|
||||
BACKEND_PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
@ -22,7 +22,7 @@ KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
# DltConnector
|
||||
DLT_CONNECTOR=true
|
||||
DLT_ACTIVE=false
|
||||
DLT_CONNECTOR_URL=http://localhost:6010
|
||||
|
||||
# Community
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
# Database
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# Klicktipp
|
||||
KLICKTIPP=false
|
||||
KLICKTTIPP_API_URL=https://api.klicktipp.com
|
||||
KLICKTIPP_USER=gradido_test
|
||||
KLICKTIPP_PASSWORD=secret321
|
||||
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
# DltConnector
|
||||
DLT_CONNECTOR=true
|
||||
DLT_CONNECTOR_URL=http://localhost:6010
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost
|
||||
COMMUNITY_REGISTER_PATH=/register
|
||||
COMMUNITY_REDEEM_PATH=/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_PATH=/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_TEST_MODUS=false
|
||||
EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
EMAIL_SMTP_HOST=gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION_PATH=/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD_PATH=/reset-password/{optin}
|
||||
EMAIL_LINK_FORGOTPASSWORD_PATH=/forgot-password
|
||||
EMAIL_LINK_OVERVIEW_PATH=/overview
|
||||
EMAIL_CODE_VALID_TIME=1440
|
||||
EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Federation
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
|
||||
|
||||
# GMS
|
||||
# GMS_ACTIVE=true
|
||||
# Coordinates of Illuminz test instance
|
||||
#GMS_API_URL=http://54.176.169.179:3071
|
||||
GMS_API_URL=http://localhost:4044
|
||||
GMS_DASHBOARD_URL=http://localhost:8080
|
||||
|
||||
# HUMHUB
|
||||
HUMHUB_ACTIVE=true
|
||||
HUMHUB_API_URL=https://community-test.gradido.net
|
||||
HUMHUB_JWT_KEY=GwdkIKi-rkRS0mXC4Cg3MYc3ktZh89VFmntDpNKET_dUfcIdjL_957F3nCv3brNtDfbbV81NViKaktUsfExrkH
|
||||
@ -1,11 +1,11 @@
|
||||
# must match the CONFIG_VERSION.EXPECTED definition in scr/config/index.ts
|
||||
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
|
||||
|
||||
# Server
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
JWT_EXPIRES_IN=$JWT_EXPIRES_IN
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=$GDT_API_URL
|
||||
BACKEND_PORT=$BACKEND_PORT
|
||||
|
||||
# Database
|
||||
DB_HOST=127.0.0.1
|
||||
@ -24,7 +24,7 @@ KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
|
||||
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
|
||||
|
||||
# DltConnector
|
||||
DLT_CONNECTOR=$DLT_CONNECTOR
|
||||
DLT_ACTIVE=$DLT_ACTIVE
|
||||
DLT_CONNECTOR_PORT=$DLT_CONNECTOR_PORT
|
||||
|
||||
# Community
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
# Server
|
||||
JWT_EXPIRES_IN=2m
|
||||
BACKEND_PORT=4000
|
||||
|
||||
GDT_ACTIVE=false
|
||||
HUMHUB_ACTIVE=false
|
||||
|
||||
@ -17,7 +17,7 @@ ENV BUILD_COMMIT="0000000"
|
||||
## SET NODE_ENV
|
||||
ENV NODE_ENV=production
|
||||
## App relevant Envs
|
||||
ENV PORT="4000"
|
||||
ENV BACKEND_PORT="4000"
|
||||
## Timezone
|
||||
ENV TZ=UTC
|
||||
ENV DB_HOST=mariadb
|
||||
@ -42,7 +42,7 @@ LABEL maintainer="support@gradido.net"
|
||||
|
||||
# Settings
|
||||
## Expose Container Port
|
||||
EXPOSE ${PORT}
|
||||
EXPOSE ${BACKEND_PORT}
|
||||
|
||||
## Workdir
|
||||
RUN mkdir -p ${DOCKER_WORKDIR}
|
||||
@ -114,8 +114,7 @@ COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/backend/build/worker.js ./wo
|
||||
# add node_modules from production_node_modules
|
||||
COPY --chown=app:app --from=production-node-modules ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
|
||||
# Copy locales
|
||||
COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/backend/locales ./locales
|
||||
COPY --chown=app:app --from=build ${DOCKER_WORKDIR}/core/build/templates ./templates
|
||||
|
||||
# Run command
|
||||
CMD ["node", "index.js"]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.3",
|
||||
"private": false,
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -8,7 +8,7 @@
|
||||
"author": "Gradido Academy - https://www.gradido.net",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "ts-node ./esbuild.config.ts && mkdirp build/templates/ && ncp src/emails/templates build/templates && mkdirp locales/ && ncp src/locales locales",
|
||||
"build": "ts-node ./esbuild.config.ts && mkdirp build/templates/ && ncp ../core/build/templates build/templates",
|
||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts,pug,json,css -r tsconfig-paths/register src/index.ts",
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_backend jest --runInBand --forceExit --detectOpenHandles",
|
||||
"test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_backend jest --coverage --runInBand --forceExit --detectOpenHandles",
|
||||
@ -19,8 +19,6 @@
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"lint:fix:unsafe": "biome check --fix --unsafe",
|
||||
"locales": "scripts/sort.sh",
|
||||
"locales:fix": "scripts/sort.sh --fix",
|
||||
"start": "cross-env TZ=UTC node build/index.js",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
@ -49,7 +47,6 @@
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/sodium-native": "^2.3.5",
|
||||
"@types/source-map-support": "^0.5.10",
|
||||
"@types/uuid": "^8.3.4",
|
||||
@ -83,12 +80,11 @@
|
||||
"log4js": "^6.7.1",
|
||||
"mkdirp": "^3.0.1",
|
||||
"ncp": "^2.0.0",
|
||||
"nodemailer": "^6.6.5",
|
||||
"nodemon": "^2.0.7",
|
||||
"openai": "^4.87.3",
|
||||
"prettier": "^3.5.3",
|
||||
"pug": "^3.0.2",
|
||||
"random-bigint": "^0.0.1",
|
||||
"redis-semaphore": "^5.6.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
"shared": "*",
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
def walk(f):
|
||||
. as $in
|
||||
| if type == "object" then
|
||||
reduce keys_unsorted[] as $key
|
||||
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
|
||||
elif type == "array" then map( walk(f) ) | f
|
||||
else f
|
||||
end;
|
||||
|
||||
def keys_sort_by(f):
|
||||
to_entries | sort_by(.key|f ) | from_entries;
|
||||
|
||||
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)
|
||||
@ -1,147 +1,22 @@
|
||||
import { Transaction as DbTransaction } from 'database'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { DltConnectorClient } from './DltConnectorClient'
|
||||
|
||||
let con: DataSource
|
||||
|
||||
let testEnv: {
|
||||
con: DataSource
|
||||
}
|
||||
|
||||
// Mock the GraphQLClient
|
||||
jest.mock('graphql-request', () => {
|
||||
const originalModule = jest.requireActual('graphql-request')
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
GraphQLClient: jest.fn().mockImplementation((url: string) => {
|
||||
if (url === 'invalid') {
|
||||
throw new Error('invalid url')
|
||||
}
|
||||
return {
|
||||
// why not using mockResolvedValueOnce or mockReturnValueOnce?
|
||||
// I have tried, but it didn't work and return every time the first value
|
||||
request: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
transmitTransaction: {
|
||||
succeed: true,
|
||||
},
|
||||
})
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('undefined DltConnectorClient', () => {
|
||||
it('invalid url', () => {
|
||||
CONFIG.DLT_CONNECTOR_URL = 'invalid'
|
||||
CONFIG.DLT_CONNECTOR = true
|
||||
CONFIG.DLT_CONNECTOR_URL = ''
|
||||
CONFIG.DLT_ACTIVE = true
|
||||
const result = DltConnectorClient.getInstance()
|
||||
expect(result).toBeUndefined()
|
||||
CONFIG.DLT_CONNECTOR_URL = 'http://dlt-connector:6010'
|
||||
})
|
||||
|
||||
it('DLT_CONNECTOR is false', () => {
|
||||
CONFIG.DLT_CONNECTOR = false
|
||||
it('DLT_ACTIVE is false', () => {
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
const result = DltConnectorClient.getInstance()
|
||||
expect(result).toBeUndefined()
|
||||
CONFIG.DLT_CONNECTOR = true
|
||||
CONFIG.DLT_ACTIVE = true
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
describe.skip('transmitTransaction, without db connection', () => {
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = 2 // Example transaction type ID
|
||||
transaction.amount = new Decimal('10.00') // Example amount
|
||||
transaction.balanceDate = new Date() // Example creation date
|
||||
transaction.id = 1 // Example transaction ID
|
||||
|
||||
it('cannot query for transaction id', async () => {
|
||||
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
describe('transmitTransaction', () => {
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
})
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = 2 // Example transaction type ID
|
||||
transaction.amount = new Decimal('10.00') // Example amount
|
||||
transaction.balanceDate = new Date() // Example creation date
|
||||
transaction.id = 1 // Example transaction ID
|
||||
|
||||
// data needed to let save succeed
|
||||
transaction.memo = "I'm a dummy memo"
|
||||
transaction.userId = 1
|
||||
transaction.userGradidoID = 'dummy gradido id'
|
||||
|
||||
/*
|
||||
it.skip('cannot find transaction in db', async () => {
|
||||
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
*/
|
||||
|
||||
it('invalid transaction type', async () => {
|
||||
const localTransaction = new DbTransaction()
|
||||
localTransaction.typeId = 12
|
||||
try {
|
||||
await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction)
|
||||
} catch (e) {
|
||||
expect(e).toMatchObject(
|
||||
new LogError(`invalid transaction type id: ${localTransaction.typeId.toString()}`),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
it.skip('should transmit the transaction and update the dltTransactionId in the database', async () => {
|
||||
await transaction.save()
|
||||
|
||||
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it.skip('invalid dltTransactionId (maximal 32 Bytes in Binary)', async () => {
|
||||
await transaction.save()
|
||||
|
||||
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
/*
|
||||
describe.skip('try transmitTransaction but graphql request failed', () => {
|
||||
it('graphql request should throw', async () => {
|
||||
const transaction = new DbTransaction()
|
||||
transaction.typeId = 2 // Example transaction type ID
|
||||
transaction.amount = new Decimal('10.00') // Example amount
|
||||
transaction.balanceDate = new Date() // Example creation date
|
||||
transaction.id = 1 // Example transaction ID
|
||||
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
*/
|
||||
|
||||
@ -1,36 +1,12 @@
|
||||
import { Transaction as DbTransaction } from 'database'
|
||||
import { GraphQLClient, gql } from 'graphql-request'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { TransactionTypeId } from 'core'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
import { TransactionResult } from './model/TransactionResult'
|
||||
import { UserIdentifier } from './model/UserIdentifier'
|
||||
import { TransactionDraft } from './model/TransactionDraft'
|
||||
import { IRestResponse, RestClient } from 'typed-rest-client'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector`)
|
||||
|
||||
const sendTransaction = gql`
|
||||
mutation ($input: TransactionInput!) {
|
||||
sendTransaction(data: $input) {
|
||||
dltTransactionIdHex
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// from ChatGPT
|
||||
function getTransactionTypeString(id: TransactionTypeId): string {
|
||||
const key = Object.keys(TransactionTypeId).find(
|
||||
(key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id,
|
||||
)
|
||||
if (key === undefined) {
|
||||
throw new LogError('invalid transaction type id: ' + id.toString())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
@ -40,7 +16,7 @@ function getTransactionTypeString(id: TransactionTypeId): string {
|
||||
|
||||
export class DltConnectorClient {
|
||||
private static instance: DltConnectorClient
|
||||
client: GraphQLClient
|
||||
client: RestClient
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
@ -55,7 +31,7 @@ export class DltConnectorClient {
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(): DltConnectorClient | undefined {
|
||||
if (!CONFIG.DLT_CONNECTOR || !CONFIG.DLT_CONNECTOR_URL) {
|
||||
if (!CONFIG.DLT_ACTIVE || !CONFIG.DLT_CONNECTOR_URL) {
|
||||
logger.info(`dlt-connector are disabled via config...`)
|
||||
return
|
||||
}
|
||||
@ -64,13 +40,12 @@ export class DltConnectorClient {
|
||||
}
|
||||
if (!DltConnectorClient.instance.client) {
|
||||
try {
|
||||
DltConnectorClient.instance.client = new GraphQLClient(CONFIG.DLT_CONNECTOR_URL, {
|
||||
method: 'GET',
|
||||
jsonSerializer: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify,
|
||||
},
|
||||
})
|
||||
DltConnectorClient.instance.client = new RestClient(
|
||||
'gradido-backend',
|
||||
CONFIG.DLT_CONNECTOR_URL,
|
||||
undefined,
|
||||
{ keepAlive: true }
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error("couldn't connect to dlt-connector: ", e)
|
||||
return
|
||||
@ -80,47 +55,14 @@ export class DltConnectorClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* transmit transaction via dlt-connector to iota
|
||||
* and update dltTransactionId of transaction in db with iota message id
|
||||
* transmit transaction via dlt-connector to hiero
|
||||
* and update dltTransactionId of transaction in db with hiero transaction id
|
||||
*/
|
||||
public async transmitTransaction(transaction: DbTransaction): Promise<boolean> {
|
||||
const typeString = getTransactionTypeString(transaction.typeId)
|
||||
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
|
||||
const amountString = transaction.amount.abs().toString()
|
||||
const params = {
|
||||
input: {
|
||||
user: {
|
||||
uuid: transaction.userGradidoID,
|
||||
communityUuid: transaction.userCommunityUuid,
|
||||
} as UserIdentifier,
|
||||
linkedUser: {
|
||||
uuid: transaction.linkedUserGradidoID,
|
||||
communityUuid: transaction.linkedUserCommunityUuid,
|
||||
} as UserIdentifier,
|
||||
amount: amountString,
|
||||
type: typeString,
|
||||
createdAt: transaction.balanceDate.toISOString(),
|
||||
backendTransactionId: transaction.id,
|
||||
targetDate: transaction.creationDate?.toISOString(),
|
||||
},
|
||||
}
|
||||
try {
|
||||
// TODO: add account nr for user after they have also more than one account in backend
|
||||
logger.debug('transmit transaction to dlt connector', params)
|
||||
const {
|
||||
data: {
|
||||
sendTransaction: { error, succeed },
|
||||
},
|
||||
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(
|
||||
sendTransaction,
|
||||
params,
|
||||
public async sendTransaction(input: TransactionDraft): Promise<IRestResponse<{ transactionId: string }>> {
|
||||
logger.debug('transmit transaction or user to dlt connector', input)
|
||||
return await this.client.create<{ transactionId: string }>(
|
||||
'/sendTransaction',
|
||||
input
|
||||
)
|
||||
if (error) {
|
||||
throw new Error(error.message)
|
||||
}
|
||||
return succeed
|
||||
} catch (e) {
|
||||
throw new LogError('Error send sending transaction to dlt-connector: ', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,55 @@
|
||||
import { CONFIG } from '@/config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
import { TransactionDraft } from '@/apis/dltConnector/model/TransactionDraft'
|
||||
import { IRestResponse } from 'typed-rest-client'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.dltConnector`)
|
||||
|
||||
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
|
||||
// and ../federation/client/FederationClientFactory.ts
|
||||
/**
|
||||
* A Singleton class defines the `getInstance` method that lets clients access
|
||||
* the unique singleton instance.
|
||||
*/
|
||||
|
||||
export class DltConnectorClient {
|
||||
private static instance: DltConnectorClient
|
||||
/**
|
||||
* The Singleton's constructor should always be private to prevent direct
|
||||
* construction calls with the `new` operator.
|
||||
*/
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* The static method that controls the access to the singleton instance.
|
||||
*
|
||||
* This implementation let you subclass the Singleton class while keeping
|
||||
* just one instance of each subclass around.
|
||||
*/
|
||||
public static getInstance(): DltConnectorClient | undefined {
|
||||
if (!CONFIG.DLT_ACTIVE || !CONFIG.DLT_CONNECTOR_URL) {
|
||||
logger.info(`dlt-connector are disabled via config...`)
|
||||
return
|
||||
}
|
||||
if (!DltConnectorClient.instance) {
|
||||
DltConnectorClient.instance = new DltConnectorClient()
|
||||
}
|
||||
return DltConnectorClient.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* transmit transaction via dlt-connector to hiero
|
||||
* and update dltTransactionId of transaction in db with hiero transaction id
|
||||
*/
|
||||
public async sendTransaction(input: TransactionDraft): Promise<IRestResponse<string>> {
|
||||
logger.debug('transmit transaction or user to dlt connector', input)
|
||||
return Promise.resolve({
|
||||
statusCode: 200,
|
||||
result: 'test',
|
||||
headers: {},
|
||||
})
|
||||
}
|
||||
}
|
||||
9
backend/src/apis/dltConnector/enum/AccountType.ts
Normal file
9
backend/src/apis/dltConnector/enum/AccountType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum AccountType {
|
||||
NONE = 'NONE', // if no address was found
|
||||
COMMUNITY_HUMAN = 'COMMUNITY_HUMAN', // creation account for human
|
||||
COMMUNITY_GMW = 'COMMUNITY_GMW', // community public budget account
|
||||
COMMUNITY_AUF = 'COMMUNITY_AUF', // community compensation and environment founds account
|
||||
COMMUNITY_PROJECT = 'COMMUNITY_PROJECT', // no creations allowed
|
||||
SUBACCOUNT = 'SUBACCOUNT', // no creations allowed
|
||||
CRYPTO_ACCOUNT = 'CRYPTO_ACCOUNT', // user control his keys, no creations
|
||||
}
|
||||
9
backend/src/apis/dltConnector/enum/DltTransactionType.ts
Normal file
9
backend/src/apis/dltConnector/enum/DltTransactionType.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export enum DltTransactionType {
|
||||
UNKNOWN = 0,
|
||||
REGISTER_ADDRESS = 1,
|
||||
CREATION = 2,
|
||||
TRANSFER = 3,
|
||||
DEFERRED_TRANSFER = 4,
|
||||
REDEEM_DEFERRED_TRANSFER = 5,
|
||||
DELETE_DEFERRED_TRANSFER = 6,
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Error Types for dlt-connector graphql responses
|
||||
*/
|
||||
export enum TransactionErrorType {
|
||||
NOT_IMPLEMENTED_YET = 'Not Implemented yet',
|
||||
MISSING_PARAMETER = 'Missing parameter',
|
||||
ALREADY_EXIST = 'Already exist',
|
||||
DB_ERROR = 'DB Error',
|
||||
PROTO_DECODE_ERROR = 'Proto Decode Error',
|
||||
PROTO_ENCODE_ERROR = 'Proto Encode Error',
|
||||
INVALID_SIGNATURE = 'Invalid Signature',
|
||||
LOGIC_ERROR = 'Logic Error',
|
||||
NOT_FOUND = 'Not found',
|
||||
}
|
||||
@ -1,11 +1,19 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
/**
|
||||
* Transaction Types on Blockchain
|
||||
*/
|
||||
export enum TransactionType {
|
||||
GRADIDO_TRANSFER = 1,
|
||||
GRADIDO_CREATION = 2,
|
||||
GROUP_FRIENDS_UPDATE = 3,
|
||||
REGISTER_ADDRESS = 4,
|
||||
GRADIDO_DEFERRED_TRANSFER = 5,
|
||||
COMMUNITY_ROOT = 6,
|
||||
GRADIDO_TRANSFER = 'GRADIDO_TRANSFER',
|
||||
GRADIDO_CREATION = 'GRADIDO_CREATION',
|
||||
GROUP_FRIENDS_UPDATE = 'GROUP_FRIENDS_UPDATE',
|
||||
REGISTER_ADDRESS = 'REGISTER_ADDRESS',
|
||||
GRADIDO_DEFERRED_TRANSFER = 'GRADIDO_DEFERRED_TRANSFER',
|
||||
GRADIDO_REDEEM_DEFERRED_TRANSFER = 'GRADIDO_REDEEM_DEFERRED_TRANSFER',
|
||||
COMMUNITY_ROOT = 'COMMUNITY_ROOT',
|
||||
}
|
||||
|
||||
registerEnumType(TransactionType, {
|
||||
name: 'TransactionType', // this one is mandatory
|
||||
description: 'Type of the transaction', // this one is optional
|
||||
})
|
||||
|
||||
166
backend/src/apis/dltConnector/index.ts
Normal file
166
backend/src/apis/dltConnector/index.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { IRestResponse } from 'typed-rest-client'
|
||||
import { DltTransactionType } from './enum/DltTransactionType'
|
||||
import { getLogger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { DltConnectorClient } from './DltConnectorClient'
|
||||
import {
|
||||
Community as DbCommunity,
|
||||
Contribution as DbContribution,
|
||||
DltTransaction as DbDltTransaction,
|
||||
TransactionLink as DbTransactionLink,
|
||||
User as DbUser,
|
||||
getCommunityByUuid,
|
||||
getHomeCommunity,
|
||||
getUserById,
|
||||
UserLoggingView,
|
||||
} from 'database'
|
||||
import { TransactionDraft } from './model/TransactionDraft'
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector`)
|
||||
// will be undefined if dlt connect is disabled
|
||||
const dltConnectorClient = DltConnectorClient.getInstance()
|
||||
|
||||
async function checkDltConnectorResult(dltTransaction: DbDltTransaction, clientResponse: Promise<IRestResponse<{ transactionId: string }>>)
|
||||
: Promise<DbDltTransaction> {
|
||||
// check result from dlt connector
|
||||
try {
|
||||
const response = await clientResponse
|
||||
if (response.statusCode === 200 && response.result) {
|
||||
dltTransaction.messageId = response.result.transactionId
|
||||
} else {
|
||||
dltTransaction.error = `empty result with status code ${response.statusCode}`
|
||||
logger.error('error from dlt-connector', response)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug(e)
|
||||
if (e instanceof Error) {
|
||||
dltTransaction.error = e.message
|
||||
logger.error('Error from dlt-connector', e)
|
||||
} else if (typeof e === 'string') {
|
||||
dltTransaction.error = e
|
||||
logger.error('error from dlt-connector', e)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
return dltTransaction
|
||||
}
|
||||
|
||||
async function executeDltTransaction(draft: TransactionDraft | null, typeId: DltTransactionType, persist = true): Promise<DbDltTransaction | null> {
|
||||
if (draft && dltConnectorClient) {
|
||||
const clientResponse = dltConnectorClient.sendTransaction(draft)
|
||||
let dltTransaction = new DbDltTransaction()
|
||||
dltTransaction.typeId = typeId
|
||||
dltTransaction = await checkDltConnectorResult(dltTransaction, clientResponse)
|
||||
if (persist) {
|
||||
return await dltTransaction.save()
|
||||
}
|
||||
return dltTransaction
|
||||
}
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* send register address transaction via dlt-connector to hiero
|
||||
* and update dltTransactionId of transaction in db with hiero transaction id
|
||||
*/
|
||||
export async function registerAddressTransaction(user: DbUser, community: DbCommunity): Promise<DbDltTransaction | null> {
|
||||
if (!CONFIG.DLT_ACTIVE) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
if (!user.id) {
|
||||
logger.error(`missing id for user: ${user.gradidoID}, please call registerAddressTransaction after user.save()`)
|
||||
return null
|
||||
}
|
||||
// return null if some data where missing and log error
|
||||
const draft = TransactionDraft.createRegisterAddress(user, community)
|
||||
const dltTransaction = await executeDltTransaction(draft, DltTransactionType.REGISTER_ADDRESS, false)
|
||||
if (dltTransaction) {
|
||||
if (user.id) {
|
||||
dltTransaction.userId = user.id
|
||||
}
|
||||
return await dltTransaction.save()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function contributionTransaction(
|
||||
contribution: DbContribution,
|
||||
signingUser: DbUser,
|
||||
createdAt: Date,
|
||||
): Promise<DbDltTransaction | null> {
|
||||
if (!CONFIG.DLT_ACTIVE) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
const homeCommunity = await getHomeCommunity()
|
||||
if (!homeCommunity) {
|
||||
logger.error('home community not found')
|
||||
return null
|
||||
}
|
||||
const draft = TransactionDraft.createContribution(contribution, createdAt, signingUser, homeCommunity)
|
||||
return await executeDltTransaction(draft, DltTransactionType.CREATION)
|
||||
}
|
||||
|
||||
export async function transferTransaction(
|
||||
senderUser: DbUser,
|
||||
recipientUser: DbUser,
|
||||
amount: string,
|
||||
memo: string,
|
||||
createdAt: Date
|
||||
): Promise<DbDltTransaction | null> {
|
||||
if (!CONFIG.DLT_ACTIVE) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
// load community if not already loaded, maybe they are remote communities
|
||||
if (!senderUser.community) {
|
||||
senderUser.community = await getCommunityByUuid(senderUser.communityUuid)
|
||||
}
|
||||
if (!recipientUser.community) {
|
||||
recipientUser.community = await getCommunityByUuid(recipientUser.communityUuid)
|
||||
}
|
||||
logger.info(`sender user: ${new UserLoggingView(senderUser)}`)
|
||||
logger.info(`recipient user: ${new UserLoggingView(recipientUser)}`)
|
||||
const draft = TransactionDraft.createTransfer(senderUser, recipientUser, amount, memo, createdAt)
|
||||
return await executeDltTransaction(draft, DltTransactionType.TRANSFER)
|
||||
}
|
||||
|
||||
export async function deferredTransferTransaction(senderUser: DbUser, transactionLink: DbTransactionLink)
|
||||
: Promise<DbDltTransaction | null> {
|
||||
if (!CONFIG.DLT_ACTIVE) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
// load community if not already loaded
|
||||
if (!senderUser.community) {
|
||||
senderUser.community = await getCommunityByUuid(senderUser.communityUuid)
|
||||
}
|
||||
const draft = TransactionDraft.createDeferredTransfer(senderUser, transactionLink)
|
||||
return await executeDltTransaction(draft, DltTransactionType.DEFERRED_TRANSFER)
|
||||
}
|
||||
|
||||
export async function redeemDeferredTransferTransaction(transactionLink: DbTransactionLink, amount: string, createdAt: Date, recipientUser: DbUser)
|
||||
: Promise<DbDltTransaction | null> {
|
||||
if (!CONFIG.DLT_ACTIVE) {
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
// load user and communities if not already loaded
|
||||
if (!transactionLink.user) {
|
||||
logger.debug('load sender user')
|
||||
transactionLink.user = await getUserById(transactionLink.userId, true, false)
|
||||
}
|
||||
if (!transactionLink.user.community) {
|
||||
logger.debug('load sender community')
|
||||
transactionLink.user.community = await getCommunityByUuid(transactionLink.user.communityUuid)
|
||||
}
|
||||
if (!recipientUser.community) {
|
||||
logger.debug('load recipient community')
|
||||
recipientUser.community = await getCommunityByUuid(recipientUser.communityUuid)
|
||||
}
|
||||
logger.debug(`sender: ${new UserLoggingView(transactionLink.user)}`)
|
||||
logger.debug(`recipient: ${new UserLoggingView(recipientUser)}`)
|
||||
const draft = TransactionDraft.redeemDeferredTransfer(transactionLink, amount, createdAt, recipientUser)
|
||||
return await executeDltTransaction(draft, DltTransactionType.REDEEM_DEFERRED_TRANSFER)
|
||||
}
|
||||
|
||||
|
||||
|
||||
15
backend/src/apis/dltConnector/model/AccountIdentifier.ts
Normal file
15
backend/src/apis/dltConnector/model/AccountIdentifier.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { CommunityAccountIdentifier } from './CommunityAccountIdentifier'
|
||||
export class AccountIdentifier {
|
||||
communityTopicId: string
|
||||
account?: CommunityAccountIdentifier
|
||||
seed?: string // used for deferred transfers
|
||||
|
||||
constructor(communityTopicId: string, input: CommunityAccountIdentifier | string) {
|
||||
if (input instanceof CommunityAccountIdentifier) {
|
||||
this.account = input
|
||||
} else {
|
||||
this.seed = input
|
||||
}
|
||||
this.communityTopicId = communityTopicId
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
export class CommunityAccountIdentifier {
|
||||
// for community user, uuid and communityUuid used
|
||||
userUuid: string
|
||||
accountNr?: number
|
||||
|
||||
constructor(userUuid: string, accountNr?: number) {
|
||||
this.userUuid = userUuid
|
||||
this.accountNr = accountNr
|
||||
}
|
||||
}
|
||||
132
backend/src/apis/dltConnector/model/TransactionDraft.ts
Executable file
132
backend/src/apis/dltConnector/model/TransactionDraft.ts
Executable file
@ -0,0 +1,132 @@
|
||||
// https://www.npmjs.com/package/@apollo/protobufjs
|
||||
import { AccountType } from '@dltConnector/enum/AccountType'
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
|
||||
import { AccountIdentifier } from './AccountIdentifier'
|
||||
import {
|
||||
Community as DbCommunity,
|
||||
Contribution as DbContribution,
|
||||
TransactionLink as DbTransactionLink,
|
||||
User as DbUser
|
||||
} from 'database'
|
||||
import { CommunityAccountIdentifier } from './CommunityAccountIdentifier'
|
||||
import { getLogger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { CODE_VALID_DAYS_DURATION } from '@/graphql/resolver/const/const'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.dltConnector.model.TransactionDraft`)
|
||||
|
||||
export class TransactionDraft {
|
||||
user: AccountIdentifier
|
||||
// not used for simply register address
|
||||
linkedUser?: AccountIdentifier
|
||||
// not used for register address
|
||||
amount?: string
|
||||
memo?: string
|
||||
type: TransactionType
|
||||
createdAt: string
|
||||
// only for creation transaction
|
||||
targetDate?: string
|
||||
// only for deferred transaction, duration in seconds
|
||||
timeoutDuration?: number
|
||||
// only for register address
|
||||
accountType?: AccountType
|
||||
|
||||
static createRegisterAddress(user: DbUser, community: DbCommunity): TransactionDraft | null {
|
||||
if (community.hieroTopicId) {
|
||||
const draft = new TransactionDraft()
|
||||
draft.user = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(user.gradidoID))
|
||||
draft.type = TransactionType.REGISTER_ADDRESS
|
||||
draft.createdAt = user.createdAt.toISOString()
|
||||
draft.accountType = AccountType.COMMUNITY_HUMAN
|
||||
return draft
|
||||
} else {
|
||||
logger.warn(`missing topicId for community ${community.id}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static createContribution(contribution: DbContribution, createdAt: Date, signingUser: DbUser, community: DbCommunity): TransactionDraft | null {
|
||||
if (community.hieroTopicId) {
|
||||
const draft = new TransactionDraft()
|
||||
draft.user = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(contribution.user.gradidoID))
|
||||
draft.linkedUser = new AccountIdentifier(community.hieroTopicId, new CommunityAccountIdentifier(signingUser.gradidoID))
|
||||
draft.type = TransactionType.GRADIDO_CREATION
|
||||
draft.createdAt = createdAt.toISOString()
|
||||
draft.amount = contribution.amount.toString()
|
||||
draft.memo = contribution.memo
|
||||
draft.targetDate = contribution.contributionDate.toISOString()
|
||||
return draft
|
||||
} else {
|
||||
logger.warn(`missing topicId for community ${community.id}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static createTransfer(sendingUser: DbUser, receivingUser: DbUser, amount: string, memo: string, createdAt: Date): TransactionDraft | null {
|
||||
if (!sendingUser.community || !receivingUser.community) {
|
||||
throw new Error(`missing community for user ${sendingUser.id} and/or ${receivingUser.id}`)
|
||||
}
|
||||
const senderUserTopic = sendingUser.community.hieroTopicId
|
||||
const receiverUserTopic = receivingUser.community.hieroTopicId
|
||||
if (!senderUserTopic || !receiverUserTopic) {
|
||||
throw new Error(`missing topicId for community ${sendingUser.community.id} and/or ${receivingUser.community.id}`)
|
||||
}
|
||||
const draft = new TransactionDraft()
|
||||
draft.user = new AccountIdentifier(senderUserTopic, new CommunityAccountIdentifier(sendingUser.gradidoID))
|
||||
draft.linkedUser = new AccountIdentifier(receiverUserTopic, new CommunityAccountIdentifier(receivingUser.gradidoID))
|
||||
draft.type = TransactionType.GRADIDO_TRANSFER
|
||||
draft.createdAt = createdAt.toISOString()
|
||||
draft.amount = amount
|
||||
draft.memo = memo
|
||||
return draft
|
||||
}
|
||||
|
||||
static createDeferredTransfer(sendingUser: DbUser, transactionLink: DbTransactionLink)
|
||||
: TransactionDraft | null {
|
||||
if (!sendingUser.community) {
|
||||
throw new Error(`missing community for user ${sendingUser.id}`)
|
||||
}
|
||||
const senderUserTopic = sendingUser.community.hieroTopicId
|
||||
if (!senderUserTopic) {
|
||||
throw new Error(`missing topicId for community ${sendingUser.community.id}`)
|
||||
}
|
||||
const createdAtOnlySeconds = transactionLink.createdAt
|
||||
createdAtOnlySeconds.setMilliseconds(0)
|
||||
const draft = new TransactionDraft()
|
||||
draft.user = new AccountIdentifier(senderUserTopic, new CommunityAccountIdentifier(sendingUser.gradidoID))
|
||||
draft.linkedUser = new AccountIdentifier(senderUserTopic, transactionLink.code)
|
||||
draft.type = TransactionType.GRADIDO_DEFERRED_TRANSFER
|
||||
draft.createdAt = createdAtOnlySeconds.toISOString()
|
||||
draft.amount = transactionLink.amount.toString()
|
||||
draft.memo = transactionLink.memo
|
||||
draft.timeoutDuration = CODE_VALID_DAYS_DURATION * 24 * 60 * 60
|
||||
return draft
|
||||
}
|
||||
|
||||
static redeemDeferredTransfer(transactionLink: DbTransactionLink, amount: string, createdAt: Date, recipientUser: DbUser): TransactionDraft | null {
|
||||
if (!transactionLink.user.community) {
|
||||
throw new Error(`missing community for user ${transactionLink.user.id}`)
|
||||
}
|
||||
if (!recipientUser.community) {
|
||||
throw new Error(`missing community for user ${recipientUser.id}`)
|
||||
}
|
||||
const senderUserTopic = transactionLink.user.community.hieroTopicId
|
||||
if (!senderUserTopic) {
|
||||
throw new Error(`missing topicId for community ${transactionLink.user.community.id}`)
|
||||
}
|
||||
const recipientUserTopic = recipientUser.community.hieroTopicId
|
||||
if (!recipientUserTopic) {
|
||||
throw new Error(`missing topicId for community ${recipientUser.community.id}`)
|
||||
}
|
||||
const createdAtOnlySeconds = createdAt
|
||||
createdAtOnlySeconds.setMilliseconds(0)
|
||||
const draft = new TransactionDraft()
|
||||
draft.user = new AccountIdentifier(senderUserTopic, transactionLink.code)
|
||||
draft.linkedUser = new AccountIdentifier(recipientUserTopic, new CommunityAccountIdentifier(recipientUser.gradidoID))
|
||||
draft.type = TransactionType.GRADIDO_REDEEM_DEFERRED_TRANSFER
|
||||
draft.createdAt = createdAtOnlySeconds.toISOString()
|
||||
draft.amount = amount
|
||||
return draft
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { TransactionErrorType } from '@dltConnector/enum/TransactionErrorType'
|
||||
|
||||
export interface TransactionError {
|
||||
type: TransactionErrorType
|
||||
message: string
|
||||
name: string
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { TransactionType } from '@dltConnector/enum/TransactionType'
|
||||
|
||||
export interface TransactionRecipe {
|
||||
id: number
|
||||
createdAt: string
|
||||
type: TransactionType
|
||||
topic: string
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { TransactionError } from './TransactionError'
|
||||
import { TransactionRecipe } from './TransactionRecipe'
|
||||
|
||||
export interface TransactionResult {
|
||||
error?: TransactionError
|
||||
recipe?: TransactionRecipe
|
||||
succeed: boolean
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export interface UserIdentifier {
|
||||
uuid: string
|
||||
communityUuid: string
|
||||
accountNr?: number
|
||||
}
|
||||
@ -4,7 +4,7 @@ import { User as DbUser } from 'database'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
// import { createGmsUser } from '@/apis/gms/GmsClient'
|
||||
// import { GmsUser } from '@/apis/gms/model/GmsUser'
|
||||
import { CONFIG } from '@/config'
|
||||
import { CONFIG as CORE_CONFIG } from 'core'
|
||||
import { sendUserToGms } from '@/graphql/resolver/util/sendUserToGms'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { initLogging } from '@/server/logger'
|
||||
@ -13,7 +13,7 @@ import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.apis.gms.ExportUsers`)
|
||||
|
||||
CONFIG.EMAIL = false
|
||||
CORE_CONFIG.EMAIL = false
|
||||
// use force to copy over all user even if gmsRegistered is set to true
|
||||
const forceMode = process.argv.includes('--force')
|
||||
|
||||
|
||||
@ -92,6 +92,7 @@ export class OpenaiClient {
|
||||
if (openaiThreadEntity.updatedAt < new Date(Date.now() - OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS * 24 * 60 * 60 * 1000)) {
|
||||
logger.info(`Openai thread for user: ${user.id} is older than ${OPENAI_AI_THREAD_DEFAULT_TIMEOUT_DAYS} days, deleting...`)
|
||||
// let run async, because it could need some time, but we don't need to wait, because we create a new one nevertheless
|
||||
// biome-ignore lint/complexity/noVoid: start it intentionally async without waiting for result
|
||||
void this.deleteThread(openaiThreadEntity.id)
|
||||
return []
|
||||
}
|
||||
|
||||
@ -16,7 +16,8 @@ const logging = {
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT ?? 4000,
|
||||
BACKEND_PORT: process.env.BACKEND_PORT ?? 4000,
|
||||
DLT_ACTIVE: process.env.DLT_ACTIVE === 'true' || false,
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
|
||||
REDEEM_JWT_TOKEN_EXPIRATION: process.env.REDEEM_JWT_TOKEN_EXPIRATION ?? '10m',
|
||||
@ -41,8 +42,7 @@ const COMMUNITY_URL = process.env.COMMUNITY_URL ?? `${URL_PROTOCOL}://${COMMUNIT
|
||||
const DLT_CONNECTOR_PORT = process.env.DLT_CONNECTOR_PORT ?? 6010
|
||||
|
||||
const dltConnector = {
|
||||
DLT_CONNECTOR: process.env.DLT_CONNECTOR === 'true' || false,
|
||||
DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? `${COMMUNITY_URL}:${DLT_CONNECTOR_PORT}`,
|
||||
DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? `http://localhost:${DLT_CONNECTOR_PORT}`,
|
||||
}
|
||||
|
||||
const community = {
|
||||
@ -63,22 +63,10 @@ const loginServer = {
|
||||
}
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true',
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true',
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER ?? 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '',
|
||||
EMAIL_SMTP_HOST: process.env.EMAIL_SMTP_HOST ?? 'mailserver',
|
||||
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
|
||||
|
||||
EMAIL_TLS: process.env.EMAIL_TLS !== 'false',
|
||||
EMAIL_LINK_VERIFICATION:
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_VERIFICATION_PATH ?? '/checkEmail/'),
|
||||
EMAIL_LINK_SETPASSWORD:
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_SETPASSWORD_PATH ?? '/reset-password/'),
|
||||
EMAIL_LINK_FORGOTPASSWORD:
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_FORGOTPASSWORD_PATH ?? '/forgot-password'),
|
||||
EMAIL_LINK_OVERVIEW: COMMUNITY_URL + (process.env.EMAIL_LINK_OVERVIEW_PATH ?? '/overview'),
|
||||
// time in minutes a optin code is valid
|
||||
EMAIL_CODE_VALID_TIME: process.env.EMAIL_CODE_VALID_TIME
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
COMMUNITY_SUPPORT_MAIL,
|
||||
COMMUNITY_URL,
|
||||
DECAY_START_TIME,
|
||||
DLT_ACTIVE,
|
||||
GDT_ACTIVE,
|
||||
GDT_API_URL,
|
||||
GMS_ACTIVE,
|
||||
@ -27,6 +28,7 @@ export const schema = Joi.object({
|
||||
COMMUNITY_DESCRIPTION,
|
||||
COMMUNITY_SUPPORT_MAIL,
|
||||
DECAY_START_TIME,
|
||||
DLT_ACTIVE,
|
||||
GDT_API_URL,
|
||||
GDT_ACTIVE,
|
||||
GMS_ACTIVE,
|
||||
@ -68,86 +70,11 @@ export const schema = Joi.object({
|
||||
.default('http://0.0.0.0/redeem/CL-')
|
||||
.required(),
|
||||
|
||||
DLT_CONNECTOR: Joi.boolean()
|
||||
.description('Flag to indicate if DLT-Connector is used. (Still in development)')
|
||||
.default(false)
|
||||
.required(),
|
||||
|
||||
DLT_CONNECTOR_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.default('http://localhost:6010')
|
||||
.when('DLT_CONNECTOR', { is: true, then: Joi.required() })
|
||||
.description('The URL for GDT API endpoint'),
|
||||
|
||||
EMAIL: Joi.boolean()
|
||||
.default(false)
|
||||
.description('Enable or disable email functionality')
|
||||
.required(),
|
||||
|
||||
EMAIL_TEST_MODUS: Joi.boolean()
|
||||
.default(false)
|
||||
.description('When enabled, all emails are sended to EMAIL_TEST_RECEIVER')
|
||||
.optional(),
|
||||
|
||||
EMAIL_TEST_RECEIVER: Joi.string()
|
||||
.email()
|
||||
.default('stage1@gradido.net')
|
||||
.when('EMAIL_TEST_MODUS', { is: true, then: Joi.required() })
|
||||
.description('Email address used in test mode'),
|
||||
|
||||
EMAIL_USERNAME: Joi.alternatives().conditional(Joi.ref('EMAIL'), {
|
||||
is: true,
|
||||
then: Joi.alternatives().conditional(Joi.ref('NODE_ENV'), {
|
||||
is: 'development',
|
||||
then: Joi.string()
|
||||
.allow('')
|
||||
.description('Username for SMTP authentication (optional in development)'),
|
||||
otherwise: Joi.string()
|
||||
.pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
|
||||
.description('Valid SMTP username required in production')
|
||||
.required(),
|
||||
}),
|
||||
otherwise: Joi.string().allow('').optional(),
|
||||
}),
|
||||
|
||||
EMAIL_SENDER: Joi.string()
|
||||
.email()
|
||||
.when('EMAIL', { is: true, then: Joi.required() })
|
||||
.default('info@gradido.net')
|
||||
.description('Email address used as sender'),
|
||||
|
||||
EMAIL_PASSWORD: Joi.alternatives().conditional(Joi.ref('EMAIL'), {
|
||||
is: true,
|
||||
then: Joi.alternatives().conditional(Joi.ref('NODE_ENV'), {
|
||||
is: 'development',
|
||||
then: Joi.string()
|
||||
.allow('')
|
||||
.description('Password for SMTP authentication (optional in development)'),
|
||||
otherwise: Joi.string()
|
||||
.min(8)
|
||||
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#]).{8,}$/)
|
||||
.description(
|
||||
'Password must be at least 8 characters long, include uppercase and lowercase letters, a number, and a special character',
|
||||
)
|
||||
.required(),
|
||||
}),
|
||||
otherwise: Joi.string().allow('').optional(),
|
||||
}),
|
||||
|
||||
EMAIL_SMTP_HOST: Joi.string()
|
||||
.hostname()
|
||||
.when('EMAIL', { is: true, then: Joi.required() })
|
||||
.default('mailserver')
|
||||
.description('SMTP server hostname'),
|
||||
|
||||
EMAIL_SMTP_PORT: Joi.number()
|
||||
.integer()
|
||||
.positive()
|
||||
.when('EMAIL', { is: true, then: Joi.required() })
|
||||
.default(1025)
|
||||
.description('SMTP server port'),
|
||||
|
||||
EMAIL_TLS: Joi.boolean().default(true).description('Enable or disable TLS for SMTP').optional(),
|
||||
.when('DLT_ACTIVE', { is: true, then: Joi.required() })
|
||||
.description('The URL for DLT connector'),
|
||||
|
||||
EMAIL_LINK_VERIFICATION: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
@ -171,17 +98,6 @@ export const schema = Joi.object({
|
||||
.description('Email Verification link for set initial Password.')
|
||||
.required(),
|
||||
|
||||
EMAIL_LINK_FORGOTPASSWORD: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.description('Email Verification link for set new Password, when old Password was forgotten.')
|
||||
.required(),
|
||||
|
||||
EMAIL_LINK_OVERVIEW: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
@ -256,7 +172,7 @@ export const schema = Joi.object({
|
||||
})
|
||||
.description('JWT key for HumHub integration, must be the same as configured in humhub'),
|
||||
|
||||
PORT: Joi.number()
|
||||
BACKEND_PORT: Joi.number()
|
||||
.integer()
|
||||
.min(1024)
|
||||
.max(49151)
|
||||
|
||||
@ -1,225 +0,0 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { decimalSeparatorByLanguage } from 'core'
|
||||
|
||||
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||
|
||||
export interface ContributionEmailCommonData {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
contributionMemo: string
|
||||
contributionFrontendLink: string
|
||||
}
|
||||
|
||||
function toContributionEmailLocales(data: ContributionEmailCommonData): Record<string, unknown> {
|
||||
return {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
contributionMemo: data.contributionMemo,
|
||||
contributionFrontendLink: data.contributionFrontendLink,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
}
|
||||
}
|
||||
|
||||
export const sendAddedContributionMessageEmail = (
|
||||
data: ContributionEmailCommonData & {
|
||||
message: string
|
||||
},
|
||||
): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: {
|
||||
to: `${data.firstName} ${data.lastName} <${data.email}>`,
|
||||
},
|
||||
template: 'addedContributionMessage',
|
||||
locals: {
|
||||
...toContributionEmailLocales(data),
|
||||
message: data.message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendAccountActivationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
activationLink: string
|
||||
timeDurationObject: Record<string, unknown>
|
||||
logoUrl?: string | null
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'accountActivation',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
activationLink: data.activationLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
logoUrl: data.logoUrl,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendAccountMultiRegistrationEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'accountMultiRegistration',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionConfirmedEmail = (
|
||||
data: ContributionEmailCommonData & {
|
||||
contributionAmount: Decimal
|
||||
},
|
||||
): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionConfirmed',
|
||||
locals: {
|
||||
...toContributionEmailLocales(data),
|
||||
contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionChangedByModeratorEmail = (
|
||||
data: ContributionEmailCommonData & {
|
||||
contributionMemoUpdated: string
|
||||
},
|
||||
): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionChangedByModerator',
|
||||
locals: {
|
||||
...toContributionEmailLocales(data),
|
||||
contributionMemoUpdated: data.contributionMemoUpdated,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionDeletedEmail = (
|
||||
data: ContributionEmailCommonData,
|
||||
): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionDeleted',
|
||||
locals: toContributionEmailLocales(data),
|
||||
})
|
||||
}
|
||||
|
||||
export const sendContributionDeniedEmail = (
|
||||
data: ContributionEmailCommonData,
|
||||
): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'contributionDenied',
|
||||
locals: toContributionEmailLocales(data),
|
||||
})
|
||||
}
|
||||
|
||||
export const sendResetPasswordEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
resetLink: string
|
||||
timeDurationObject: Record<string, unknown>
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'resetPassword',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
resetLink: data.resetLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
senderEmail: string
|
||||
transactionMemo: string
|
||||
transactionAmount: Decimal
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'transactionLinkRedeemed',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
senderEmail: data.senderEmail,
|
||||
transactionMemo: data.transactionMemo,
|
||||
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const sendTransactionReceivedEmail = (data: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
language: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
senderEmail: string
|
||||
memo: string
|
||||
transactionAmount: Decimal
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||
template: 'transactionReceived',
|
||||
locals: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
memo: data.memo,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
senderEmail: data.senderEmail,
|
||||
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -4,4 +4,5 @@ export interface PublicCommunityInfo {
|
||||
creationDate: Date
|
||||
publicKey: string
|
||||
publicJwtKey: string
|
||||
hieroTopicId: string | null
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { DataSource, Not } from 'typeorm'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
|
||||
import { AppDatabase } from 'database'
|
||||
import { validateCommunities } from './validateCommunities'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`)
|
||||
@ -16,21 +16,25 @@ const federationClientLogger = getLogger(
|
||||
)
|
||||
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('validate Communities', () => {
|
||||
|
||||
@ -138,6 +138,7 @@ async function writeForeignCommunity(
|
||||
com.publicKey = dbCom.publicKey
|
||||
com.publicJwtKey = pubInfo.publicJwtKey
|
||||
com.url = dbCom.endPoint
|
||||
com.hieroTopicId = pubInfo.hieroTopicId
|
||||
await DbCommunity.save(com)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { ContributionCycleType } from 'database'
|
||||
|
||||
// lowercase values are not implemented yet
|
||||
export enum ContributionCycleType {
|
||||
ONCE = 'ONCE',
|
||||
HOUR = 'hour',
|
||||
TWO_HOURS = 'two_hours',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
EIGHT_HOURS = 'eight_hours',
|
||||
HALF_DAY = 'half_day',
|
||||
DAILY = 'DAILY',
|
||||
TWO_DAYS = 'two_days',
|
||||
THREE_DAYS = 'three_days',
|
||||
FOUR_DAYS = 'four_days',
|
||||
FIVE_DAYS = 'five_days',
|
||||
SIX_DAYS = 'six_days',
|
||||
WEEK = 'week',
|
||||
TWO_WEEKS = 'two_weeks',
|
||||
MONTH = 'month',
|
||||
TWO_MONTH = 'two_month',
|
||||
QUARTER = 'quarter',
|
||||
HALF_YEAR = 'half_year',
|
||||
YEAR = 'year',
|
||||
}
|
||||
export { ContributionCycleType }
|
||||
|
||||
registerEnumType(ContributionCycleType, {
|
||||
name: 'ContributionCycleType', // this one is mandatory
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { ContributionStatus } from 'database'
|
||||
|
||||
export enum ContributionStatus {
|
||||
PENDING = 'PENDING',
|
||||
DELETED = 'DELETED',
|
||||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
DENIED = 'DENIED',
|
||||
CONFIRMED = 'CONFIRMED',
|
||||
}
|
||||
export { ContributionStatus }
|
||||
|
||||
registerEnumType(ContributionStatus, {
|
||||
name: 'ContributionStatus',
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { ContributionType } from 'database'
|
||||
|
||||
export enum ContributionType {
|
||||
ADMIN = 'ADMIN',
|
||||
USER = 'USER',
|
||||
LINK = 'LINK',
|
||||
}
|
||||
export { ContributionType }
|
||||
|
||||
registerEnumType(ContributionType, {
|
||||
name: 'ContributionType',
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { PendingTransactionState } from 'shared'
|
||||
|
||||
export { PendingTransactionState }
|
||||
|
||||
registerEnumType(PendingTransactionState, {
|
||||
name: 'PendingTransactionState', // this one is mandatory
|
||||
description: 'State of the PendingTransaction', // this one is optional
|
||||
|
||||
@ -1,13 +1,7 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
import { RoleNames } from 'database'
|
||||
|
||||
export enum RoleNames {
|
||||
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||
USER = 'USER',
|
||||
MODERATOR = 'MODERATOR',
|
||||
MODERATOR_AI = 'MODERATOR_AI',
|
||||
ADMIN = 'ADMIN',
|
||||
DLT_CONNECTOR = 'DLT_CONNECTOR_ROLE',
|
||||
}
|
||||
export { RoleNames }
|
||||
|
||||
registerEnumType(RoleNames, {
|
||||
name: 'RoleNames', // this one is mandatory
|
||||
|
||||
@ -3,6 +3,7 @@ import { ArgsType, Field, InputType } from 'type-graphql'
|
||||
|
||||
import { Location } from '@/graphql/model/Location'
|
||||
import { isValidLocation } from '@/graphql/validator/Location'
|
||||
import { isValidHieroId } from '@/graphql/validator/HieroId'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
@ -21,5 +22,6 @@ export class EditCommunityInput {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@isValidHieroId()
|
||||
hieroTopicId?: string | null
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ export class UserAdmin {
|
||||
this.emailChecked = user.emailContact?.emailChecked
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.createdAt = user.createdAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
this.roles = user.userRoles?.map((userRole) => userRole.role) ?? []
|
||||
}
|
||||
@ -41,6 +42,9 @@ export class UserAdmin {
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
emailConfirmationSend: string | null
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import { DataSource } from 'typeorm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, updateHomeCommunityQuery } from '@/seeds/graphql/mutations'
|
||||
@ -20,6 +19,7 @@ import { createCommunity, createVerifiedFederatedCommunity } from 'database/src/
|
||||
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { CONFIG } from '@/config'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -29,11 +29,12 @@ CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER = 1000
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
const peterLoginData = {
|
||||
@ -43,10 +44,11 @@ const peterLoginData = {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(getLogger('apollo'), localization)
|
||||
testEnv = await testEnvironment(getLogger('apollo'))
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
// reset id auto increment
|
||||
await DbCommunity.clear()
|
||||
@ -55,6 +57,7 @@ beforeAll(async () => {
|
||||
|
||||
afterAll(async () => {
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
// real valid ed25519 key pairs
|
||||
|
||||
@ -19,6 +19,7 @@ import { listContributionLinks } from '@/seeds/graphql/queries'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -27,10 +28,12 @@ const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -38,6 +41,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
@ -46,6 +50,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('Contribution Links', () => {
|
||||
|
||||
@ -5,10 +5,9 @@ import { DataSource } from 'typeorm'
|
||||
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
|
||||
import { sendAddedContributionMessageEmail } from 'core'
|
||||
import { EventType } from '@/event/Events'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import {
|
||||
@ -22,6 +21,7 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { getLogger} from 'config-schema/test/testSetup'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionMessageResolver`)
|
||||
const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
@ -30,36 +30,38 @@ const interactionLogger = getLogger(
|
||||
)
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
jest.mock('core', () => {
|
||||
const originalModule = jest.requireActual('core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
sendAddedContributionMessageEmail: jest.fn((a) =>
|
||||
originalModule.sendAddedContributionMessageEmail(a),
|
||||
),
|
||||
sendAddedContributionMessageEmail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
let result: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger, localization)
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('ContributionMessageResolver', () => {
|
||||
|
||||
@ -14,7 +14,7 @@ import { Order } from '@enum/Order'
|
||||
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
|
||||
import { sendAddedContributionMessageEmail } from 'core'
|
||||
import {
|
||||
EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE,
|
||||
EVENT_CONTRIBUTION_MESSAGE_CREATE,
|
||||
|
||||
@ -14,14 +14,13 @@ import {
|
||||
resetToken,
|
||||
testEnvironment,
|
||||
} from '@test/helpers'
|
||||
import { i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import {
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionDeletedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
} from 'core'
|
||||
import { EventType } from '@/event/Events'
|
||||
import { creations } from '@/seeds/creation/index'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
@ -53,8 +52,19 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { getFirstDayOfPreviousNMonth } from 'core'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { getLogger as originalGetLogger } from 'log4js'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants')
|
||||
jest.mock('core', () => {
|
||||
const originalModule = jest.requireActual('core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
sendContributionDeniedEmail: jest.fn(),
|
||||
sendContributionConfirmedEmail: jest.fn(),
|
||||
sendContributionDeletedEmail: jest.fn(),
|
||||
sendEmailTranslated: jest.fn(),
|
||||
}
|
||||
})
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
@ -62,10 +72,12 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
let creation: Contribution | null
|
||||
let admin: User
|
||||
@ -77,16 +89,18 @@ let contributionToDelete: any
|
||||
let bibiCreatedContribution: Contribution
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(originalGetLogger('apollo'), localization)
|
||||
testEnv = await testEnvironment(originalGetLogger('apollo'))
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('ContributionResolver', () => {
|
||||
@ -2130,14 +2144,6 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('stores the EMAIL_CONFIRMATION event in the database', async () => {
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_CONFIRMATION,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('confirm same contribution again', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import {
|
||||
AppDatabase,
|
||||
Contribution as DbContribution,
|
||||
Transaction as DbTransaction,
|
||||
User as DbUser,
|
||||
getLastTransaction,
|
||||
UserContact,
|
||||
} from 'database'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
@ -9,26 +11,7 @@ import { GraphQLResolveInfo } from 'graphql'
|
||||
import { Arg, Args, Authorized, Ctx, Info, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
import { EntityManager, IsNull } from 'typeorm'
|
||||
|
||||
import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
|
||||
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
|
||||
import { ContributionArgs } from '@arg/ContributionArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { ContributionType } from '@enum/ContributionType'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { OpenCreation } from '@model/OpenCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { TransactionTypeId } from 'core'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import {
|
||||
sendContributionChangedByModeratorEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionDeletedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import {
|
||||
EVENT_ADMIN_CONTRIBUTION_CONFIRM,
|
||||
EVENT_ADMIN_CONTRIBUTION_CREATE,
|
||||
@ -42,14 +25,32 @@ import {
|
||||
import { UpdateUnconfirmedContributionContext } from '@/interactions/updateUnconfirmedContribution/UpdateUnconfirmedContribution.context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import { TRANSACTIONS_LOCK } from 'database'
|
||||
import { fullName } from 'core'
|
||||
import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
|
||||
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
|
||||
import { ContributionArgs } from '@arg/ContributionArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { SearchContributionsFilterArgs } from '@arg/SearchContributionsFilterArgs'
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { ContributionType } from '@enum/ContributionType'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { OpenCreation } from '@model/OpenCreation'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import {
|
||||
fullName,
|
||||
sendContributionChangedByModeratorEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionDeletedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
TransactionTypeId
|
||||
} from 'core'
|
||||
import { calculateDecay, Decay } from 'shared'
|
||||
|
||||
import { contributionTransaction } from '@/apis/dltConnector'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { AppDatabase } from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { Mutex } from 'redis-semaphore'
|
||||
import {
|
||||
contributionFrontendLink,
|
||||
loadAllContributions,
|
||||
@ -58,8 +59,6 @@ import {
|
||||
import { getOpenCreations, getUserCreation, validateContribution } from './util/creations'
|
||||
import { extractGraphQLFields } from './util/extractGraphQLFields'
|
||||
import { findContributions } from './util/findContributions'
|
||||
import { getLastTransaction } from 'database'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.ContributionResolver`)
|
||||
@ -435,12 +434,13 @@ export class ContributionResolver {
|
||||
): Promise<boolean> {
|
||||
const logger = createLogger()
|
||||
logger.addContext('contribution', id)
|
||||
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
const mutex = new Mutex (db.getRedisClient(), 'TRANSACTIONS_LOCK')
|
||||
await mutex.acquire()
|
||||
|
||||
try {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const contribution = await DbContribution.findOne({ where: { id } })
|
||||
const contribution = await DbContribution.findOne({ where: { id }, relations: {user: {emailContact: true}} })
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
@ -450,18 +450,17 @@ export class ContributionResolver {
|
||||
if (contribution.contributionStatus === 'DENIED') {
|
||||
throw new LogError('Contribution already denied', id)
|
||||
}
|
||||
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
throw new LogError('Moderator can not confirm own contribution')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail({
|
||||
where: { id: contribution.userId },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
const user = contribution.user
|
||||
if (user.deletedAt) {
|
||||
throw new LogError('Can not confirm contribution since the user was deleted')
|
||||
}
|
||||
const receivedCallDate = new Date()
|
||||
const dltTransactionPromise = contributionTransaction(contribution, moderatorUser, receivedCallDate)
|
||||
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
|
||||
validateContribution(
|
||||
creations,
|
||||
@ -470,11 +469,9 @@ export class ContributionResolver {
|
||||
clientTimezoneOffset,
|
||||
)
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
const queryRunner = db.getDataSource().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||
|
||||
const lastTransaction = await getLastTransaction(contribution.userId)
|
||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||
|
||||
@ -491,7 +488,7 @@ export class ContributionResolver {
|
||||
}
|
||||
newBalance = newBalance.add(contribution.amount.toString())
|
||||
|
||||
const transaction = new DbTransaction()
|
||||
let transaction = new DbTransaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = contribution.memo
|
||||
transaction.userId = contribution.userId
|
||||
@ -509,7 +506,7 @@ export class ContributionResolver {
|
||||
transaction.balanceDate = receivedCallDate
|
||||
transaction.decay = decay ? decay.decay : new Decimal(0)
|
||||
transaction.decayStart = decay ? decay.start : null
|
||||
await queryRunner.manager.insert(DbTransaction, transaction)
|
||||
transaction = await queryRunner.manager.save(DbTransaction, transaction)
|
||||
|
||||
contribution.confirmedAt = receivedCallDate
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
@ -519,9 +516,6 @@ export class ContributionResolver {
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
|
||||
// trigger to send transaction via dlt-connector
|
||||
await sendTransactionsToDltConnector()
|
||||
|
||||
logger.info('creation commited successfuly.')
|
||||
await sendContributionConfirmedEmail({
|
||||
firstName: user.firstName,
|
||||
@ -537,6 +531,17 @@ export class ContributionResolver {
|
||||
contribution.createdAt,
|
||||
),
|
||||
})
|
||||
|
||||
// update transaction id in dlt transaction tables
|
||||
// wait for finishing transaction by dlt-connector/hiero
|
||||
const dltStartTime = new Date()
|
||||
const dltTransaction = await dltTransactionPromise
|
||||
if(dltTransaction) {
|
||||
dltTransaction.transactionId = transaction.id
|
||||
await dltTransaction.save()
|
||||
}
|
||||
const dltEndTime = new Date()
|
||||
logger.debug(`dlt-connector contribution finished in ${dltEndTime.getTime() - dltStartTime.getTime()} ms`)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError('Creation was not successful', e)
|
||||
@ -545,7 +550,8 @@ export class ContributionResolver {
|
||||
}
|
||||
await EVENT_ADMIN_CONTRIBUTION_CONFIRM(user, moderatorUser, contribution, contribution.amount)
|
||||
} finally {
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -5,35 +5,41 @@ import { DataSource } from 'typeorm'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
|
||||
import { CONFIG as CORE_CONFIG } from 'core'
|
||||
import { CONFIG } from '@/config'
|
||||
import { writeHomeCommunityEntry } from '@/seeds/community'
|
||||
import { createUser, forgotPassword, setPassword } from '@/seeds/graphql/mutations'
|
||||
import { queryOptIn } from '@/seeds/graphql/queries'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
CONFIG.EMAIL_CODE_VALID_TIME = 1440
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = 10
|
||||
CONFIG.EMAIL = false
|
||||
CORE_CONFIG.EMAIL = false
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('EmailOptinCodes', () => {
|
||||
|
||||
@ -2,7 +2,6 @@ import { Event as DbEvent, UserContact } from 'database'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { i18n as localization } from '@test/testSetup'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
|
||||
import { EventType } from '@/event/Events'
|
||||
@ -10,6 +9,7 @@ import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
@ -18,17 +18,20 @@ const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.Klicktip
|
||||
let testEnv: any
|
||||
let mutate: any
|
||||
let con: any
|
||||
let db: AppDatabase
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger, localization)
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('KlicktippResolver', () => {
|
||||
|
||||
@ -32,27 +32,31 @@ import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
|
||||
import { transactionLinks } from '@/seeds/transactionLink/index'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { TRANSACTIONS_LOCK } from 'database'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { transactionLinkCode } from './TransactionLinkResolver'
|
||||
import { CONFIG } from '@/config'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
const logErrorLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
|
||||
// mock semaphore to allow use fake timers
|
||||
jest.mock('database/src/util/TRANSACTIONS_LOCK')
|
||||
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||
// jest.mock('database/src/util/TRANSACTIONS_LOCK')
|
||||
// TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
let user: User
|
||||
@ -62,6 +66,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
@ -70,6 +75,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('TransactionLinkResolver', () => {
|
||||
@ -944,7 +950,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
describe('without any filters', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
it('finds 7 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -954,14 +960,14 @@ describe('TransactionLinkResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
count: 6,
|
||||
count: 7,
|
||||
links: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
memo: 'Leider wollte niemand meine Gradidos haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
memo: "Kein Trick, keine Zauberrei,\n bei Gradidio sei dabei!",
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
@ -973,7 +979,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
describe('all filters are null', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
it('finds 7 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -990,10 +996,10 @@ describe('TransactionLinkResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
count: 6,
|
||||
count: 7,
|
||||
links: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
memo: 'Leider wollte niemand meine Gradidos haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@ -1009,7 +1015,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
describe('filter with deleted', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
it('finds 7 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -1024,10 +1030,10 @@ describe('TransactionLinkResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
count: 7,
|
||||
count: 8,
|
||||
links: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
memo: 'Leider wollte niemand meine Gradidos haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@ -1043,7 +1049,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
describe('filter by expired', () => {
|
||||
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
|
||||
it('finds 6 open transaction links, 1 expired, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -1061,7 +1067,7 @@ describe('TransactionLinkResolver', () => {
|
||||
count: 7,
|
||||
links: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
memo: 'Leider wollte niemand meine Gradidos haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
@ -1079,7 +1085,7 @@ describe('TransactionLinkResolver', () => {
|
||||
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
|
||||
|
||||
describe.skip('filter by redeemed', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
it('finds 7 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -1096,10 +1102,10 @@ describe('TransactionLinkResolver', () => {
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
count: 6,
|
||||
count: 7,
|
||||
links: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
memo: 'Leider wollte niemand meine Gradidos haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
|
||||
@ -15,9 +15,13 @@ import { QueryLinkResult } from '@union/QueryLinkResult'
|
||||
import { Decay, interpretEncryptedTransferArgs, TransactionTypeId } from 'core'
|
||||
import {
|
||||
AppDatabase, Contribution as DbContribution,
|
||||
ContributionLink as DbContributionLink, FederatedCommunity as DbFederatedCommunity, Transaction as DbTransaction,
|
||||
ContributionLink as DbContributionLink,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
DltTransaction as DbDltTransaction,
|
||||
Transaction as DbTransaction,
|
||||
TransactionLink as DbTransactionLink,
|
||||
User as DbUser,
|
||||
findModeratorCreatingContributionLink,
|
||||
findTransactionLinkByCode,
|
||||
getHomeCommunity
|
||||
} from 'database'
|
||||
@ -35,9 +39,17 @@ import { LogError } from '@/server/LogError'
|
||||
import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
import { fullName } from 'core'
|
||||
import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database'
|
||||
import { calculateDecay, compoundInterest, decayFormula, decode, DisburseJwtPayloadType, encode, encryptAndSign, EncryptedJWEJwtPayloadType, RedeemJwtPayloadType, verify } from 'shared'
|
||||
|
||||
// import { TRANSACTION_LINK_LOCK, TRANSACTIONS_LOCK } from 'database'
|
||||
import {
|
||||
calculateDecay,
|
||||
compoundInterest,
|
||||
decode,
|
||||
DisburseJwtPayloadType,
|
||||
encode,
|
||||
encryptAndSign,
|
||||
RedeemJwtPayloadType,
|
||||
verify
|
||||
} from 'shared'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { DisbursementClient as V1_0_DisbursementClient } from '@/federation/client/1_0/DisbursementClient'
|
||||
import { DisbursementClientFactory } from '@/federation/client/DisbursementClientFactory'
|
||||
@ -52,9 +64,12 @@ import {
|
||||
getCommunityByUuid,
|
||||
} from './util/communities'
|
||||
import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
import { transactionLinkList } from './util/transactionLinkList'
|
||||
import { SignedTransferPayloadType } from 'shared'
|
||||
import { contributionTransaction, deferredTransferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector'
|
||||
import { CODE_VALID_DAYS_DURATION } from './const/const'
|
||||
import { Redis } from 'ioredis'
|
||||
import { Mutex } from 'redis-semaphore'
|
||||
|
||||
const createLogger = (method: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionLinkResolver.${method}`)
|
||||
|
||||
@ -68,7 +83,7 @@ export const transactionLinkCode = (date: Date): string => {
|
||||
)
|
||||
}
|
||||
|
||||
const CODE_VALID_DAYS_DURATION = 14
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
|
||||
export const transactionLinkExpireDate = (date: Date): Date => {
|
||||
@ -106,11 +121,20 @@ export class TransactionLinkResolver {
|
||||
transactionLink.code = transactionLinkCode(createdDate)
|
||||
transactionLink.createdAt = createdDate
|
||||
transactionLink.validUntil = validUntil
|
||||
const dltTransactionPromise = deferredTransferTransaction(user, transactionLink)
|
||||
await DbTransactionLink.save(transactionLink).catch((e) => {
|
||||
throw new LogError('Unable to save transaction link', e)
|
||||
})
|
||||
await EVENT_TRANSACTION_LINK_CREATE(user, transactionLink, amount)
|
||||
|
||||
// wait for dlt transaction to be created
|
||||
const startTime = Date.now()
|
||||
const dltTransaction = await dltTransactionPromise
|
||||
const endTime = Date.now()
|
||||
createLogger('createTransactionLink').debug(`dlt transaction created in ${endTime - startTime} ms`)
|
||||
if (dltTransaction) {
|
||||
dltTransaction.transactionLinkId = transactionLink.id
|
||||
await DbDltTransaction.save(dltTransaction)
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user))
|
||||
}
|
||||
|
||||
@ -134,7 +158,6 @@ export class TransactionLinkResolver {
|
||||
user.id,
|
||||
)
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
|
||||
}
|
||||
@ -143,7 +166,19 @@ export class TransactionLinkResolver {
|
||||
throw new LogError('Transaction link could not be deleted', e)
|
||||
})
|
||||
|
||||
transactionLink.user = user
|
||||
const dltTransactionPromise = redeemDeferredTransferTransaction(transactionLink, transactionLink.amount.toString(), transactionLink.deletedAt!, user)
|
||||
|
||||
await EVENT_TRANSACTION_LINK_DELETE(user, transactionLink)
|
||||
// wait for dlt transaction to be created
|
||||
const startTime = Date.now()
|
||||
const dltTransaction = await dltTransactionPromise
|
||||
const endTime = Date.now()
|
||||
createLogger('deleteTransactionLink').debug(`dlt transaction created in ${endTime - startTime} ms`)
|
||||
if (dltTransaction) {
|
||||
dltTransaction.transactionLinkId = transactionLink.id
|
||||
await DbDltTransaction.save(dltTransaction)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -204,7 +239,9 @@ export class TransactionLinkResolver {
|
||||
const user = getUser(context)
|
||||
if (code.match(/^CL-/)) {
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
// const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTIONS_LOCK')
|
||||
await mutex.acquire()
|
||||
try {
|
||||
methodLogger.info('redeem contribution link...')
|
||||
const now = new Date()
|
||||
@ -276,7 +313,7 @@ export class TransactionLinkResolver {
|
||||
throw new LogError('Contribution link has unknown cycle', contributionLink.cycle)
|
||||
}
|
||||
}
|
||||
|
||||
const moderatorPromise = findModeratorCreatingContributionLink(contributionLink)
|
||||
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
methodLogger.info('open creations', creations)
|
||||
validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset)
|
||||
@ -290,6 +327,12 @@ export class TransactionLinkResolver {
|
||||
contribution.contributionType = ContributionType.LINK
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
|
||||
let dltTransactionPromise: Promise<DbDltTransaction | null> = Promise.resolve(null)
|
||||
const moderator = await moderatorPromise
|
||||
if (moderator) {
|
||||
dltTransactionPromise = contributionTransaction(contribution, moderator, now)
|
||||
}
|
||||
|
||||
await queryRunner.manager.insert(DbContribution, contribution)
|
||||
|
||||
const lastTransaction = await getLastTransaction(user.id)
|
||||
@ -335,6 +378,17 @@ export class TransactionLinkResolver {
|
||||
contributionLink,
|
||||
contributionLink.amount,
|
||||
)
|
||||
if (dltTransactionPromise) {
|
||||
const startTime = new Date()
|
||||
const dltTransaction = await dltTransactionPromise
|
||||
const endTime = new Date()
|
||||
methodLogger.info(`dlt-connector transaction finished in ${endTime.getTime() - startTime.getTime()} ms`)
|
||||
if (dltTransaction) {
|
||||
dltTransaction.transactionId = transaction.id
|
||||
await dltTransaction.save()
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError('Creation from contribution link was not successful', e)
|
||||
@ -342,14 +396,15 @@ export class TransactionLinkResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
} finally {
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
// trigger to send transaction via dlt-connector
|
||||
await sendTransactionsToDltConnector()
|
||||
return true
|
||||
} else {
|
||||
// const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire()
|
||||
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTION_LINK_LOCK')
|
||||
await mutex.acquire()
|
||||
const now = new Date()
|
||||
const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire()
|
||||
try {
|
||||
const transactionLink = await DbTransactionLink.findOne({ where: { code } })
|
||||
if (!transactionLink) {
|
||||
@ -393,7 +448,8 @@ export class TransactionLinkResolver {
|
||||
transactionLink.amount,
|
||||
)
|
||||
} finally {
|
||||
releaseLinkLock()
|
||||
// releaseLinkLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -33,10 +33,15 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { CONFIG } from '@/config'
|
||||
import { CONFIG as CORE_CONFIG} from 'core'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.LogError`)
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
CORE_CONFIG.EMAIL = false
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
@ -45,6 +50,7 @@ let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -58,6 +64,7 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy() // close()
|
||||
await testEnv.db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
let bobData: any
|
||||
@ -69,7 +76,6 @@ let peter: User
|
||||
|
||||
let homeCom: DbCommunity
|
||||
let foreignCom: DbCommunity
|
||||
let fedForeignCom: DbFederatedCommunity
|
||||
|
||||
describe('send coins', () => {
|
||||
beforeAll(async () => {
|
||||
@ -434,50 +440,6 @@ describe('send coins', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('sendTransactionsToDltConnector', () => {
|
||||
let transaction: Transaction[]
|
||||
let dltTransactions: DltTransaction[]
|
||||
beforeAll(async () => {
|
||||
// Find the previous created transactions of sendCoin mutation
|
||||
transaction = await Transaction.find({
|
||||
where: { memo: 'unrepeatable memo' },
|
||||
order: { balanceDate: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
// and read aslong as all async created dlt-transactions are finished
|
||||
do {
|
||||
dltTransactions = await DltTransaction.find({
|
||||
where: { transactionId: In([transaction[0].id, transaction[1].id]) },
|
||||
// relations: ['transaction'],
|
||||
// order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
} while (transaction.length > dltTransactions.length)
|
||||
})
|
||||
|
||||
it('has wait till sendTransactionsToDltConnector created all dlt-transactions', () => {
|
||||
expect(dltTransactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transaction[0].id,
|
||||
messageId: null,
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transaction[1].id,
|
||||
messageId: null,
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('send coins via gradido ID', () => {
|
||||
it('sends the coins', async () => {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import {
|
||||
AppDatabase,
|
||||
countOpenPendingTransactions,
|
||||
Community as DbCommunity,
|
||||
DltTransaction as DbDltTransaction,
|
||||
Transaction as dbTransaction,
|
||||
TransactionLink as dbTransactionLink,
|
||||
User as dbUser,
|
||||
findUserByIdentifier
|
||||
findUserByIdentifier,
|
||||
} from 'database'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'
|
||||
@ -17,22 +17,23 @@ import { Order } from '@enum/Order'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionList } from '@model/TransactionList'
|
||||
import { User } from '@model/User'
|
||||
import { processXComCompleteTransaction, TransactionTypeId } from 'core'
|
||||
|
||||
import {
|
||||
fullName,
|
||||
processXComCompleteTransaction,
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
TransactionTypeId
|
||||
} from 'core'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { CONFIG } from '@/config'
|
||||
import {
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Events'
|
||||
EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Events'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
import { virtualDecayTransaction, virtualLinkTransaction } from '@/util/virtualTransactions'
|
||||
import { fullName } from 'core'
|
||||
import { TRANSACTIONS_LOCK } from 'database'
|
||||
// import { TRANSACTIONS_LOCK } from 'database'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLastTransaction } from 'database'
|
||||
@ -41,8 +42,10 @@ import { BalanceResolver } from './BalanceResolver'
|
||||
import { GdtResolver } from './GdtResolver'
|
||||
import { getCommunityName, isHomeCommunity } from './util/communities'
|
||||
import { getTransactionList } from './util/getTransactionList'
|
||||
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
|
||||
import { transactionLinkSummary } from './util/transactionLinkSummary'
|
||||
import { transferTransaction, redeemDeferredTransferTransaction } from '@/apis/dltConnector'
|
||||
import { Redis } from 'ioredis'
|
||||
import { Mutex } from 'redis-semaphore'
|
||||
|
||||
const db = AppDatabase.getInstance()
|
||||
const createLogger = () => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.TransactionResolver`)
|
||||
@ -56,7 +59,17 @@ export const executeTransaction = async (
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<boolean> => {
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
// const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
const mutex = new Mutex(db.getRedisClient(), 'TRANSACTIONS_LOCK')
|
||||
await mutex.acquire()
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
let dltTransactionPromise: Promise<DbDltTransaction | null> = Promise.resolve(null)
|
||||
if (!transactionLink) {
|
||||
dltTransactionPromise = transferTransaction(sender, recipient, amount.toString(), memo, receivedCallDate)
|
||||
} else {
|
||||
dltTransactionPromise = redeemDeferredTransferTransaction(transactionLink, amount.toString(), receivedCallDate, recipient)
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info('executeTransaction', amount, memo, sender, recipient)
|
||||
@ -72,7 +85,6 @@ export const executeTransaction = async (
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
sender.id,
|
||||
amount.mul(-1),
|
||||
@ -162,9 +174,15 @@ export const executeTransaction = async (
|
||||
transactionReceive,
|
||||
transactionReceive.amount,
|
||||
)
|
||||
|
||||
// trigger to send transaction via dlt-connector
|
||||
await sendTransactionsToDltConnector()
|
||||
// update dltTransaction with transactionId
|
||||
const startTime = new Date()
|
||||
const dltTransaction = await dltTransactionPromise
|
||||
const endTime = new Date()
|
||||
logger.debug(`dlt-connector transaction finished in ${endTime.getTime() - startTime.getTime()} ms`)
|
||||
if (dltTransaction) {
|
||||
dltTransaction.transactionId = transactionSend.id
|
||||
await dltTransaction.save()
|
||||
}
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError('Transaction was not successful', e)
|
||||
@ -197,7 +215,8 @@ export const executeTransaction = async (
|
||||
}
|
||||
logger.info(`finished executeTransaction successfully`)
|
||||
} finally {
|
||||
releaseLock()
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { UserInputError } from 'apollo-server-express'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import {
|
||||
AppDatabase,
|
||||
Community as DbCommunity,
|
||||
Event as DbEvent,
|
||||
TransactionLink,
|
||||
@ -20,7 +21,6 @@ import { UserContactType } from '@enum/UserContactType'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { Location } from '@model/Location'
|
||||
import { cleanDB, headerPushMock, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { subscribe } from '@/apis/KlicktippController'
|
||||
import { CONFIG } from '@/config'
|
||||
@ -28,7 +28,7 @@ import {
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendResetPasswordEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
} from 'core'
|
||||
import { EventType } from '@/event/Events'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
|
||||
@ -74,16 +74,15 @@ import { Location2Point } from './util/Location2Point'
|
||||
jest.mock('@/apis/humhub/HumHubClient')
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
jest.mock('core', () => {
|
||||
const originalModule = jest.requireActual('core')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)),
|
||||
sendAccountMultiRegistrationEmail: jest.fn((a) =>
|
||||
originalModule.sendAccountMultiRegistrationEmail(a),
|
||||
),
|
||||
sendResetPasswordEmail: jest.fn((a) => originalModule.sendResetPasswordEmail(a)),
|
||||
sendAccountActivationEmail: jest.fn(),
|
||||
sendAccountMultiRegistrationEmail: jest.fn(),
|
||||
sendResetPasswordEmail: jest.fn(),
|
||||
sendEmailTranslated: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
@ -105,24 +104,29 @@ let user: User
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let query: ApolloServerTestClient['query']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(getLogger('apollo'), localization)
|
||||
testEnv = await testEnvironment(getLogger('apollo'))
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
CONFIG.HUMHUB_ACTIVE = false
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('UserResolver', () => {
|
||||
@ -154,6 +158,7 @@ describe('UserResolver', () => {
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({ data: { createUser: { id: expect.any(Number) } } }),
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
@ -1039,7 +1044,7 @@ describe('UserResolver', () => {
|
||||
|
||||
describe('user exists in DB', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -1049,7 +1054,7 @@ describe('UserResolver', () => {
|
||||
|
||||
describe('duration not expired', () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
await expect(mutate({ mutation: forgotPassword, variables: { email: 'bob@baumeister.de' } })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
@ -1066,7 +1071,7 @@ describe('UserResolver', () => {
|
||||
describe('duration reset to 0', () => {
|
||||
it('returns true', async () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = 0
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
await expect(mutate({ mutation: forgotPassword, variables: { email: 'bob@baumeister.de' } })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
@ -1077,9 +1082,9 @@ describe('UserResolver', () => {
|
||||
|
||||
it('sends reset password email', () => {
|
||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bob',
|
||||
lastName: 'der Baumeister',
|
||||
email: 'bob@baumeister.de',
|
||||
language: 'de',
|
||||
resetLink: expect.any(String),
|
||||
timeDurationObject: expect.objectContaining({
|
||||
@ -1091,7 +1096,7 @@ describe('UserResolver', () => {
|
||||
|
||||
it('stores the EMAIL_FORGOT_PASSWORD event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
where: { email: 'bob@baumeister.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
@ -1107,7 +1112,7 @@ describe('UserResolver', () => {
|
||||
describe('request reset password again', () => {
|
||||
it('throws an error', async () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
await expect(mutate({ mutation: forgotPassword, variables: { email: 'bob@baumeister.de' } })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Email already sent less than 10 minutes ago')],
|
||||
}),
|
||||
@ -1127,8 +1132,8 @@ describe('UserResolver', () => {
|
||||
let emailContact: UserContact
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
emailContact = await UserContact.findOneOrFail({ where: { email: bibiBloxberg.email } })
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
emailContact = await UserContact.findOneOrFail({ where: { email: bobBaumeister.email } })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -1139,7 +1144,7 @@ describe('UserResolver', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
query({ query: queryOptIn, variables: { optIn: 'not-valid' } }),
|
||||
query({ query: queryOptIn, variables: { email: 'bob@baumeister.de', optIn: 'not-valid' } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
@ -1160,7 +1165,7 @@ describe('UserResolver', () => {
|
||||
await expect(
|
||||
query({
|
||||
query: queryOptIn,
|
||||
variables: { optIn: emailContact.emailVerificationCode.toString() },
|
||||
variables: { email: 'bob@baumeister.de', optIn: emailContact.emailVerificationCode.toString() },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
@ -2256,7 +2261,7 @@ describe('UserResolver', () => {
|
||||
relations: ['user'],
|
||||
})
|
||||
const activationLink = `${
|
||||
CONFIG.EMAIL_LINK_VERIFICATION
|
||||
CONFIG.EMAIL_LINK_SETPASSWORD
|
||||
}${userContact.emailVerificationCode.toString()}`
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
findUserByIdentifier
|
||||
} from 'database'
|
||||
import { GraphQLResolveInfo } from 'graphql'
|
||||
import i18n from 'i18n'
|
||||
import {
|
||||
Arg,
|
||||
Args,
|
||||
@ -57,11 +56,6 @@ import { encode } from '@/auth/JWT'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { CONFIG } from '@/config'
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import {
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendResetPasswordEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import {
|
||||
EVENT_ADMIN_USER_DELETE,
|
||||
EVENT_ADMIN_USER_ROLE_SET,
|
||||
@ -85,8 +79,12 @@ import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { durationInMinutesFromDates, getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
import { delay } from 'core'
|
||||
|
||||
import {
|
||||
delay,
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendResetPasswordEmail,
|
||||
} from 'core'
|
||||
import random from 'random-bigint'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
|
||||
@ -104,6 +102,7 @@ import { deleteUserRole, setUserRole } from './util/modifyUserRole'
|
||||
import { sendUserToGms } from './util/sendUserToGms'
|
||||
import { syncHumhub } from './util/syncHumhub'
|
||||
import { validateAlias } from 'core'
|
||||
import { registerAddressTransaction } from '@/apis/dltConnector'
|
||||
import { updateAllDefinedAndChanged } from 'shared'
|
||||
|
||||
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
|
||||
@ -232,7 +231,6 @@ export class UserResolver {
|
||||
logger.debug('validation of login credentials successful...')
|
||||
|
||||
const user = new User(dbUser)
|
||||
i18n.setLocale(user.language)
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||
@ -321,7 +319,6 @@ export class UserResolver {
|
||||
if (!language || !isLanguage(language)) {
|
||||
language = DEFAULT_LANGUAGE
|
||||
}
|
||||
i18n.setLocale(language)
|
||||
|
||||
// check if user with email still exists?
|
||||
email = email.trim().toLowerCase()
|
||||
@ -388,6 +385,7 @@ export class UserResolver {
|
||||
if (homeCom.communityUuid) {
|
||||
dbUser.communityUuid = homeCom.communityUuid
|
||||
}
|
||||
|
||||
dbUser.gradidoID = gradidoID
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
@ -399,7 +397,10 @@ export class UserResolver {
|
||||
}
|
||||
dbUser.publisherId = publisherId ?? 0
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
|
||||
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug('new dbUser', new UserLoggingView(dbUser))
|
||||
}
|
||||
if (redeemCode) {
|
||||
if (redeemCode.match(/^CL-/)) {
|
||||
const contributionLink = await DbContributionLink.findOne({
|
||||
@ -435,7 +436,7 @@ export class UserResolver {
|
||||
|
||||
dbUser.emailContact = emailContact
|
||||
dbUser.emailId = emailContact.id
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
throw new LogError('Error while updating dbUser', error)
|
||||
})
|
||||
|
||||
@ -467,6 +468,8 @@ export class UserResolver {
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
// register user into blockchain
|
||||
const dltTransactionPromise = registerAddressTransaction(dbUser, homeCom)
|
||||
logger.info('createUser() successful...')
|
||||
if (CONFIG.HUMHUB_ACTIVE) {
|
||||
let spaceId: number | null = null
|
||||
@ -503,6 +506,11 @@ export class UserResolver {
|
||||
}
|
||||
}
|
||||
}
|
||||
// wait for finishing dlt transaction
|
||||
const startTime = new Date()
|
||||
await dltTransactionPromise
|
||||
const endTime = new Date()
|
||||
logger.info(`dlt-connector register address finished in ${endTime.getTime() - startTime.getTime()} ms`)
|
||||
return new User(dbUser)
|
||||
}
|
||||
|
||||
@ -752,7 +760,6 @@ export class UserResolver {
|
||||
throw new LogError('Given language is not a valid language or not supported')
|
||||
}
|
||||
user.language = language
|
||||
i18n.setLocale(language)
|
||||
updated = true
|
||||
}
|
||||
|
||||
@ -969,7 +976,7 @@ export class UserResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<SearchUsersResult> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt']
|
||||
const userFields = ['id', 'firstName', 'lastName', 'emailId', 'emailContact', 'deletedAt', 'createdAt']
|
||||
const [users, count] = await findUsers(
|
||||
userFields,
|
||||
query,
|
||||
|
||||
@ -12,3 +12,4 @@ export const MEMO_MAX_CHARS = 512
|
||||
export const MEMO_MIN_CHARS = 5
|
||||
export const DEFAULT_PAGINATION_PAGE_SIZE = 25
|
||||
export const FRONTEND_CONTRIBUTIONS_ITEM_ANCHOR_PREFIX = 'contributionListItem-'
|
||||
export const CODE_VALID_DAYS_DURATION = 14
|
||||
@ -21,17 +21,25 @@ import {
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { CONFIG } from '@/config'
|
||||
import { CONFIG as CORE_CONFIG } from 'core'
|
||||
// import { TRANSACTIONS_LOCK } from 'database'
|
||||
import { Mutex } from 'redis-semaphore'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
CONFIG.DLT_ACTIVE = false
|
||||
CORE_CONFIG.EMAIL = false
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
@ -42,9 +50,45 @@ beforeAll(async () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await testEnv.db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
type WorkData = { start: number, end: number }
|
||||
async function fakeWork(workData: WorkData[], index: number) {
|
||||
// const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
// create a new mutex for every function call, like in production code
|
||||
const mutex = new Mutex(testEnv.db.getRedisClient(), 'TRANSACTIONS_LOCK')
|
||||
await mutex.acquire()
|
||||
|
||||
const startDate = new Date()
|
||||
await new Promise((resolve) => setTimeout(resolve, Math.random() * 50))
|
||||
const endDate = new Date()
|
||||
workData[index] = { start: startDate.getTime(), end: endDate.getTime() }
|
||||
// releaseLock()
|
||||
await mutex.release()
|
||||
}
|
||||
|
||||
describe('semaphore', () => {
|
||||
it("didn't should run in parallel", async () => {
|
||||
const workData: WorkData[] = []
|
||||
|
||||
const promises: Promise<void>[] = []
|
||||
for(let i = 0; i < 20; i++) {
|
||||
promises.push(fakeWork(workData, i))
|
||||
}
|
||||
await Promise.all(promises)
|
||||
workData.sort((a, b) => a.start - b.start)
|
||||
workData.forEach((work, index) => {
|
||||
expect(work.start).toBeLessThan(work.end)
|
||||
if(index < workData.length - 1) {
|
||||
expect(work.start).toBeLessThan(workData[index + 1].start)
|
||||
expect(work.end).toBeLessThanOrEqual(workData[index + 1].start)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('semaphore fullstack', () => {
|
||||
let contributionLinkCode = ''
|
||||
let bobsTransactionLinkCode = ''
|
||||
let bibisTransactionLinkCode = ''
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Contribution, User } from 'database'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
import { cleanDB, contributionDateFormatter, testEnvironment } from '@test/helpers'
|
||||
|
||||
@ -18,22 +19,26 @@ CONFIG.HUMHUB_ACTIVE = false
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
const setZeroHours = (date: Date): Date => {
|
||||
|
||||
@ -1,799 +0,0 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Community, DltTransaction, Transaction } from 'database'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
// import { GraphQLClient } from 'graphql-request'
|
||||
// import { Response } from 'graphql-request/dist/types'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { Response } from 'graphql-request/dist/types'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { TransactionTypeId } from 'core'
|
||||
import { creations } from '@/seeds/creation'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
|
||||
import { getLogger } from 'config-schema/test/testSetup'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
|
||||
import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
const logger = getLogger(
|
||||
`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`,
|
||||
)
|
||||
|
||||
/*
|
||||
// Mock the GraphQLClient
|
||||
jest.mock('graphql-request', () => {
|
||||
const originalModule = jest.requireActual('graphql-request')
|
||||
|
||||
let testCursor = 0
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
GraphQLClient: jest.fn().mockImplementation((url: string) => {
|
||||
if (url === 'invalid') {
|
||||
throw new Error('invalid url')
|
||||
}
|
||||
return {
|
||||
// why not using mockResolvedValueOnce or mockReturnValueOnce?
|
||||
// I have tried, but it didn't work and return every time the first value
|
||||
request: jest.fn().mockImplementation(() => {
|
||||
testCursor++
|
||||
if (testCursor === 4) {
|
||||
return Promise.resolve(
|
||||
// invalid, is 33 Bytes long as binary
|
||||
{
|
||||
transmitTransaction: {
|
||||
dltTransactionIdHex:
|
||||
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A',
|
||||
},
|
||||
},
|
||||
)
|
||||
} else if (testCursor === 5) {
|
||||
throw Error('Connection error')
|
||||
} else {
|
||||
return Promise.resolve(
|
||||
// valid, is 32 Bytes long as binary
|
||||
{
|
||||
transmitTransaction: {
|
||||
dltTransactionIdHex:
|
||||
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
let mutate: ApolloServerTestClient['mutate'],
|
||||
query: ApolloServerTestClient['query'],
|
||||
con: Connection
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: Connection
|
||||
}
|
||||
*/
|
||||
|
||||
async function createHomeCommunity(): Promise<Community> {
|
||||
const homeCommunity = Community.create()
|
||||
homeCommunity.foreign = false
|
||||
homeCommunity.communityUuid = uuidv4()
|
||||
homeCommunity.url = 'localhost'
|
||||
homeCommunity.publicKey = Buffer.from('0x6e6a6c6d6feffe', 'hex')
|
||||
await Community.save(homeCommunity)
|
||||
return homeCommunity
|
||||
}
|
||||
|
||||
async function createTxCREATION1(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(1000)
|
||||
tx.balance = new Decimal(100)
|
||||
tx.balanceDate = new Date('01.01.2023 00:00:00')
|
||||
tx.memo = 'txCREATION1'
|
||||
tx.typeId = TransactionTypeId.CREATION
|
||||
tx.userGradidoID = 'txCREATION1.userGradidoID'
|
||||
tx.userId = 1
|
||||
tx.userName = 'txCREATION 1'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('01.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('01.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxCREATION2(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(1000)
|
||||
tx.balance = new Decimal(200)
|
||||
tx.balanceDate = new Date('02.01.2023 00:00:00')
|
||||
tx.memo = 'txCREATION2'
|
||||
tx.typeId = TransactionTypeId.CREATION
|
||||
tx.userGradidoID = 'txCREATION2.userGradidoID'
|
||||
tx.userId = 2
|
||||
tx.userName = 'txCREATION 2'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('02.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('02.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxCREATION3(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(1000)
|
||||
tx.balance = new Decimal(300)
|
||||
tx.balanceDate = new Date('03.01.2023 00:00:00')
|
||||
tx.memo = 'txCREATION3'
|
||||
tx.typeId = TransactionTypeId.CREATION
|
||||
tx.userGradidoID = 'txCREATION3.userGradidoID'
|
||||
tx.userId = 3
|
||||
tx.userName = 'txCREATION 3'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('03.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('03.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxSend1ToReceive2(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(100)
|
||||
tx.balance = new Decimal(1000)
|
||||
tx.balanceDate = new Date('11.01.2023 00:00:00')
|
||||
tx.memo = 'txSEND1 to txRECEIVE2'
|
||||
tx.typeId = TransactionTypeId.SEND
|
||||
tx.userGradidoID = 'txSEND1.userGradidoID'
|
||||
tx.userId = 1
|
||||
tx.userName = 'txSEND 1'
|
||||
tx.linkedUserGradidoID = 'txRECEIVE2.linkedUserGradidoID'
|
||||
tx.linkedUserId = 2
|
||||
tx.linkedUserName = 'txRECEIVE 2'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('11.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a1'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('11.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxReceive2FromSend1(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(100)
|
||||
tx.balance = new Decimal(1300)
|
||||
tx.balanceDate = new Date('11.01.2023 00:00:00')
|
||||
tx.memo = 'txSEND1 to txRECEIVE2'
|
||||
tx.typeId = TransactionTypeId.RECEIVE
|
||||
tx.userGradidoID = 'txRECEIVE2.linkedUserGradidoID'
|
||||
tx.userId = 2
|
||||
tx.userName = 'txRECEIVE 2'
|
||||
tx.linkedUserGradidoID = 'txSEND1.userGradidoID'
|
||||
tx.linkedUserId = 1
|
||||
tx.linkedUserName = 'txSEND 1'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('11.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b2'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('11.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
/*
|
||||
async function createTxSend2ToReceive3(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(200)
|
||||
tx.balance = new Decimal(1100)
|
||||
tx.balanceDate = new Date('23.01.2023 00:00:00')
|
||||
tx.memo = 'txSEND2 to txRECEIVE3'
|
||||
tx.typeId = TransactionTypeId.SEND
|
||||
tx.userGradidoID = 'txSEND2.userGradidoID'
|
||||
tx.userId = 2
|
||||
tx.userName = 'txSEND 2'
|
||||
tx.linkedUserGradidoID = 'txRECEIVE3.linkedUserGradidoID'
|
||||
tx.linkedUserId = 3
|
||||
tx.linkedUserName = 'txRECEIVE 3'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('23.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a2'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('23.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxReceive3FromSend2(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(200)
|
||||
tx.balance = new Decimal(1500)
|
||||
tx.balanceDate = new Date('23.01.2023 00:00:00')
|
||||
tx.memo = 'txSEND2 to txRECEIVE3'
|
||||
tx.typeId = TransactionTypeId.RECEIVE
|
||||
tx.userGradidoID = 'txRECEIVE3.linkedUserGradidoID'
|
||||
tx.userId = 3
|
||||
tx.userName = 'txRECEIVE 3'
|
||||
tx.linkedUserGradidoID = 'txSEND2.userGradidoID'
|
||||
tx.linkedUserId = 2
|
||||
tx.linkedUserName = 'txSEND 2'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('23.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b3'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('23.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxSend3ToReceive1(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(300)
|
||||
tx.balance = new Decimal(1200)
|
||||
tx.balanceDate = new Date('31.01.2023 00:00:00')
|
||||
tx.memo = 'txSEND3 to txRECEIVE1'
|
||||
tx.typeId = TransactionTypeId.SEND
|
||||
tx.userGradidoID = 'txSEND3.userGradidoID'
|
||||
tx.userId = 3
|
||||
tx.userName = 'txSEND 3'
|
||||
tx.linkedUserGradidoID = 'txRECEIVE1.linkedUserGradidoID'
|
||||
tx.linkedUserId = 1
|
||||
tx.linkedUserName = 'txRECEIVE 1'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('31.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516a3'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('31.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
|
||||
async function createTxReceive1FromSend3(verified: boolean): Promise<Transaction> {
|
||||
let tx = Transaction.create()
|
||||
tx.amount = new Decimal(300)
|
||||
tx.balance = new Decimal(1300)
|
||||
tx.balanceDate = new Date('31.01.2023 00:00:00')
|
||||
tx.memo = 'txSEND3 to txRECEIVE1'
|
||||
tx.typeId = TransactionTypeId.RECEIVE
|
||||
tx.userGradidoID = 'txRECEIVE1.linkedUserGradidoID'
|
||||
tx.userId = 1
|
||||
tx.userName = 'txRECEIVE 1'
|
||||
tx.linkedUserGradidoID = 'txSEND3.userGradidoID'
|
||||
tx.linkedUserId = 3
|
||||
tx.linkedUserName = 'txSEND 3'
|
||||
tx = await Transaction.save(tx)
|
||||
|
||||
if (verified) {
|
||||
const dlttx = DltTransaction.create()
|
||||
dlttx.createdAt = new Date('31.01.2023 00:00:10')
|
||||
dlttx.messageId = '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516b1'
|
||||
dlttx.transactionId = tx.id
|
||||
dlttx.verified = true
|
||||
dlttx.verifiedAt = new Date('31.01.2023 00:01:10')
|
||||
await DltTransaction.save(dlttx)
|
||||
}
|
||||
return tx
|
||||
}
|
||||
*/
|
||||
|
||||
let con: DataSource
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger, localization)
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
})
|
||||
|
||||
describe('create and send Transactions to DltConnector', () => {
|
||||
let txCREATION1: Transaction
|
||||
let txCREATION2: Transaction
|
||||
let txCREATION3: Transaction
|
||||
let txSEND1to2: Transaction
|
||||
let txRECEIVE2From1: Transaction
|
||||
// let txSEND2To3: Transaction
|
||||
// let txRECEIVE3From2: Transaction
|
||||
// let txSEND3To1: Transaction
|
||||
// let txRECEIVE1From3: Transaction
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
describe('with 3 creations but inactive dlt-connector', () => {
|
||||
it('found 3 dlt-transactions', async () => {
|
||||
txCREATION1 = await createTxCREATION1(false)
|
||||
txCREATION2 = await createTxCREATION2(false)
|
||||
txCREATION3 = await createTxCREATION3(false)
|
||||
await createHomeCommunity()
|
||||
|
||||
CONFIG.DLT_CONNECTOR = false
|
||||
await sendTransactionsToDltConnector()
|
||||
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
|
||||
|
||||
// Find the previous created transactions of sendCoin mutation
|
||||
const transactions = await Transaction.find({
|
||||
// where: { memo: 'unrepeatable memo' },
|
||||
order: { balanceDate: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
const dltTransactions = await DltTransaction.find({
|
||||
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
|
||||
// relations: ['transaction'],
|
||||
order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
expect(dltTransactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[0].id,
|
||||
messageId: null,
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[1].id,
|
||||
messageId: null,
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[2].id,
|
||||
messageId: null,
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
expect(logger.info).nthCalledWith(2, 'sending to DltConnector currently not configured...')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with 3 creations and active dlt-connector', () => {
|
||||
it('found 3 dlt-transactions', async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, raeuberHotzenplotz)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
let count = 0
|
||||
for (const creation of creations) {
|
||||
await creationFactory(testEnv, creation)
|
||||
count++
|
||||
// we need only 3 for testing
|
||||
if (count >= 3) {
|
||||
break
|
||||
}
|
||||
}
|
||||
await createHomeCommunity()
|
||||
|
||||
CONFIG.DLT_CONNECTOR = true
|
||||
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
return {
|
||||
data: {
|
||||
sendTransaction: { succeed: true },
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
|
||||
await sendTransactionsToDltConnector()
|
||||
|
||||
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
|
||||
|
||||
// Find the previous created transactions of sendCoin mutation
|
||||
const transactions = await Transaction.find({
|
||||
// where: { memo: 'unrepeatable memo' },
|
||||
order: { balanceDate: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
const dltTransactions = await DltTransaction.find({
|
||||
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
|
||||
// relations: ['transaction'],
|
||||
order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
expect(dltTransactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[0].id,
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[1].id,
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: transactions[2].id,
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with 3 verified creations, 1 sendCoins and active dlt-connector', () => {
|
||||
it('found 3 dlt-transactions', async () => {
|
||||
txCREATION1 = await createTxCREATION1(true)
|
||||
txCREATION2 = await createTxCREATION2(true)
|
||||
txCREATION3 = await createTxCREATION3(true)
|
||||
await createHomeCommunity()
|
||||
|
||||
txSEND1to2 = await createTxSend1ToReceive2(false)
|
||||
txRECEIVE2From1 = await createTxReceive2FromSend1(false)
|
||||
|
||||
/*
|
||||
txSEND2To3 = await createTxSend2ToReceive3()
|
||||
txRECEIVE3From2 = await createTxReceive3FromSend2()
|
||||
txSEND3To1 = await createTxSend3ToReceive1()
|
||||
txRECEIVE1From3 = await createTxReceive1FromSend3()
|
||||
*/
|
||||
|
||||
CONFIG.DLT_CONNECTOR = true
|
||||
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
return {
|
||||
data: {
|
||||
sendTransaction: { succeed: true },
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
|
||||
await sendTransactionsToDltConnector()
|
||||
|
||||
expect(logger.info).toBeCalledWith('sendTransactionsToDltConnector...')
|
||||
|
||||
// Find the previous created transactions of sendCoin mutation
|
||||
/*
|
||||
const transactions = await Transaction.find({
|
||||
// where: { memo: 'unrepeatable memo' },
|
||||
order: { balanceDate: 'ASC', id: 'ASC' },
|
||||
})
|
||||
*/
|
||||
|
||||
const dltTransactions = await DltTransaction.find({
|
||||
// where: { transactionId: In([transaction[0].id, transaction[1].id]) },
|
||||
// relations: ['transaction'],
|
||||
order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
expect(dltTransactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txCREATION1.id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c1',
|
||||
verified: true,
|
||||
createdAt: new Date('01.01.2023 00:00:10'),
|
||||
verifiedAt: new Date('01.01.2023 00:01:10'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txCREATION2.id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c2',
|
||||
verified: true,
|
||||
createdAt: new Date('02.01.2023 00:00:10'),
|
||||
verifiedAt: new Date('02.01.2023 00:01:10'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txCREATION3.id,
|
||||
messageId: '723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516c3',
|
||||
verified: true,
|
||||
createdAt: new Date('03.01.2023 00:00:10'),
|
||||
verifiedAt: new Date('03.01.2023 00:01:10'),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txSEND1to2.id,
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
transactionId: txRECEIVE2From1.id,
|
||||
messageId: 'sended',
|
||||
verified: false,
|
||||
createdAt: expect.any(Date),
|
||||
verifiedAt: null,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
/*
|
||||
describe('with one Community of api 1_0 and not matching pubKey', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
|
||||
return {
|
||||
data: {
|
||||
getPublicKey: {
|
||||
publicKey: 'somePubKey',
|
||||
},
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
const variables1 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
await DbFederatedCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables1)
|
||||
.orUpdate({
|
||||
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
|
||||
jest.clearAllMocks()
|
||||
// await validateCommunities()
|
||||
})
|
||||
|
||||
it('logs one community found', () => {
|
||||
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
expect(logger.info).toBeCalledWith(
|
||||
'Federation: getPublicKey from endpoint',
|
||||
'http//localhost:5001/api/1_0/',
|
||||
)
|
||||
})
|
||||
it('logs not matching publicKeys', () => {
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
'Federation: received not matching publicKey:',
|
||||
'somePubKey',
|
||||
expect.stringMatching('11111111111111111111111111111111'),
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('with one Community of api 1_0 and matching pubKey', () => {
|
||||
beforeEach(async () => {
|
||||
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
|
||||
return {
|
||||
data: {
|
||||
getPublicKey: {
|
||||
publicKey: '11111111111111111111111111111111',
|
||||
},
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
const variables1 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
await DbFederatedCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables1)
|
||||
.orUpdate({
|
||||
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
await DbFederatedCommunity.update({}, { verifiedAt: null })
|
||||
jest.clearAllMocks()
|
||||
// await validateCommunities()
|
||||
})
|
||||
|
||||
it('logs one community found', () => {
|
||||
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
expect(logger.info).toBeCalledWith(
|
||||
'Federation: getPublicKey from endpoint',
|
||||
'http//localhost:5001/api/1_0/',
|
||||
)
|
||||
})
|
||||
it('logs community pubKey verified', () => {
|
||||
expect(logger.info).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'Federation: verified community with',
|
||||
'http//localhost:5001/api/',
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('with two Communities of api 1_0 and 1_1', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
|
||||
|
||||
return {
|
||||
data: {
|
||||
getPublicKey: {
|
||||
publicKey: '11111111111111111111111111111111',
|
||||
},
|
||||
},
|
||||
} as Response<unknown>
|
||||
})
|
||||
const variables2 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
apiVersion: '1_1',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
await DbFederatedCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables2)
|
||||
.orUpdate({
|
||||
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
|
||||
await DbFederatedCommunity.update({}, { verifiedAt: null })
|
||||
jest.clearAllMocks()
|
||||
// await validateCommunities()
|
||||
})
|
||||
it('logs two communities found', () => {
|
||||
expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`)
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
expect(logger.info).toBeCalledWith(
|
||||
'Federation: getPublicKey from endpoint',
|
||||
'http//localhost:5001/api/1_0/',
|
||||
)
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_1 ', () => {
|
||||
expect(logger.info).toBeCalledWith(
|
||||
'Federation: getPublicKey from endpoint',
|
||||
'http//localhost:5001/api/1_1/',
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
|
||||
let dbCom: DbFederatedCommunity
|
||||
beforeEach(async () => {
|
||||
const variables3 = {
|
||||
publicKey: Buffer.from('11111111111111111111111111111111'),
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http//localhost:5001/api/',
|
||||
lastAnnouncedAt: new Date(),
|
||||
}
|
||||
await DbFederatedCommunity.createQueryBuilder()
|
||||
.insert()
|
||||
.into(DbFederatedCommunity)
|
||||
.values(variables3)
|
||||
.orUpdate({
|
||||
|
||||
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||
overwrite: ['end_point', 'last_announced_at'],
|
||||
})
|
||||
.execute()
|
||||
dbCom = await DbFederatedCommunity.findOneOrFail({
|
||||
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
|
||||
})
|
||||
await DbFederatedCommunity.update({}, { verifiedAt: null })
|
||||
jest.clearAllMocks()
|
||||
// await validateCommunities()
|
||||
})
|
||||
it('logs three community found', () => {
|
||||
expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`)
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_0 ', () => {
|
||||
expect(logger.info).toBeCalledWith(
|
||||
'Federation: getPublicKey from endpoint',
|
||||
'http//localhost:5001/api/1_0/',
|
||||
)
|
||||
})
|
||||
it('logs requestGetPublicKey for community api 1_1 ', () => {
|
||||
expect(logger.info).toBeCalledWith(
|
||||
'Federation: getPublicKey from endpoint',
|
||||
'http//localhost:5001/api/1_1/',
|
||||
)
|
||||
})
|
||||
it('logs unsupported api for community with api 2_0 ', () => {
|
||||
expect(logger.warn).toBeCalledWith(
|
||||
'Federation: dbCom with unsupported apiVersion',
|
||||
dbCom.endPoint,
|
||||
'2_0',
|
||||
)
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
})
|
||||
@ -1,85 +0,0 @@
|
||||
import { DltTransaction, Transaction } from 'database'
|
||||
import { IsNull } from 'typeorm'
|
||||
|
||||
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { Monitor, MonitorNames } from '@/util/Monitor'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(
|
||||
`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.sendTransactionsToDltConnector`,
|
||||
)
|
||||
|
||||
export async function sendTransactionsToDltConnector(): Promise<void> {
|
||||
logger.info('sendTransactionsToDltConnector...')
|
||||
// check if this logic is still occupied, no concurrecy allowed
|
||||
if (!Monitor.isLocked(MonitorNames.SEND_DLT_TRANSACTIONS)) {
|
||||
// mark this block for occuption to prevent concurrency
|
||||
Monitor.lockIt(MonitorNames.SEND_DLT_TRANSACTIONS)
|
||||
|
||||
try {
|
||||
await createDltTransactions()
|
||||
const dltConnector = DltConnectorClient.getInstance()
|
||||
if (dltConnector) {
|
||||
logger.debug('with sending to DltConnector...')
|
||||
const dltTransactions = await DltTransaction.find({
|
||||
where: { messageId: IsNull() },
|
||||
relations: ['transaction'],
|
||||
order: { createdAt: 'ASC', id: 'ASC' },
|
||||
})
|
||||
|
||||
for (const dltTx of dltTransactions) {
|
||||
if (!dltTx.transaction) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const result = await dltConnector.transmitTransaction(dltTx.transaction)
|
||||
// message id isn't known at this point of time, because transaction will not direct sended to iota,
|
||||
// it will first go to db and then sended, if no transaction is in db before
|
||||
if (result) {
|
||||
dltTx.messageId = 'sended'
|
||||
await DltTransaction.save(dltTx)
|
||||
logger.info(`store messageId=${dltTx.messageId} in dltTx=${dltTx.id}`)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`error while sending to dlt-connector or writing messageId of dltTx=${dltTx.id}`,
|
||||
e,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info('sending to DltConnector currently not configured...')
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('error on sending transactions to dlt-connector.', e)
|
||||
} finally {
|
||||
// releae Monitor occupation
|
||||
Monitor.releaseIt(MonitorNames.SEND_DLT_TRANSACTIONS)
|
||||
}
|
||||
} else {
|
||||
logger.info('sendTransactionsToDltConnector currently locked by monitor...')
|
||||
}
|
||||
}
|
||||
|
||||
async function createDltTransactions(): Promise<void> {
|
||||
const dltqb = DltTransaction.createQueryBuilder().select('transactions_id')
|
||||
const newTransactions: Transaction[] = await Transaction.createQueryBuilder()
|
||||
.select('id')
|
||||
.addSelect('balance_date')
|
||||
.where('id NOT IN (' + dltqb.getSql() + ')')
|
||||
|
||||
.orderBy({ balance_date: 'ASC', id: 'ASC' })
|
||||
.getRawMany()
|
||||
|
||||
const dltTxArray: DltTransaction[] = []
|
||||
let idx = 0
|
||||
while (newTransactions.length > dltTxArray.length) {
|
||||
// timing problems with for(let idx = 0; idx < newTransactions.length; idx++) {
|
||||
const dltTx = DltTransaction.create()
|
||||
dltTx.transactionId = newTransactions[idx++].id
|
||||
await DltTransaction.save(dltTx)
|
||||
dltTxArray.push(dltTx)
|
||||
}
|
||||
}
|
||||
23
backend/src/graphql/validator/HieroId.ts
Normal file
23
backend/src/graphql/validator/HieroId.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { ValidationArguments, ValidationOptions, registerDecorator } from 'class-validator'
|
||||
|
||||
export function isValidHieroId(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isValidHieroId',
|
||||
target: object.constructor,
|
||||
propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: string) {
|
||||
if (value.match(/[0-9]*\.[0-9]*\.[0-9]*/)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${propertyName} must be a valid HieroId (0.0.2121), ${args.property}`
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -12,12 +12,12 @@ async function main() {
|
||||
const { app } = await createServer(getLogger('apollo'))
|
||||
|
||||
await writeJwtKeyPairInHomeCommunity()
|
||||
app.listen(CONFIG.PORT, () => {
|
||||
app.listen(CONFIG.BACKEND_PORT, () => {
|
||||
// biome-ignore lint/suspicious/noConsole: no need for logging the start message
|
||||
console.log(`Server is running at http://localhost:${CONFIG.PORT}`)
|
||||
console.log(`Server is running at http://localhost:${CONFIG.BACKEND_PORT}`)
|
||||
if (CONFIG.GRAPHIQL) {
|
||||
// biome-ignore lint/suspicious/noConsole: no need for logging the start message
|
||||
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
|
||||
console.log(`GraphIQL available at http://localhost:${CONFIG.BACKEND_PORT}`)
|
||||
}
|
||||
})
|
||||
await startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
export { ContributionLinkInterface } from 'database'
|
||||
|
||||
/*
|
||||
export interface ContributionLinkInterface {
|
||||
amount: number
|
||||
name: string
|
||||
@ -5,3 +8,4 @@ export interface ContributionLinkInterface {
|
||||
validFrom?: Date
|
||||
validTo?: Date
|
||||
}
|
||||
*/
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ContributionLinkInterface } from './ContributionLinkInterface'
|
||||
export { contributionLinks } from 'database'
|
||||
|
||||
/*
|
||||
export const contributionLinks: ContributionLinkInterface[] = [
|
||||
{
|
||||
name: 'Dokumenta 2017',
|
||||
@ -16,3 +17,4 @@ export const contributionLinks: ContributionLinkInterface[] = [
|
||||
validTo: new Date(2022, 8, 25),
|
||||
},
|
||||
]
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { CreationInterface } from 'database'
|
||||
/*
|
||||
export interface CreationInterface {
|
||||
email: string
|
||||
amount: number
|
||||
@ -7,3 +9,4 @@ export interface CreationInterface {
|
||||
// number of months to move the confirmed creation to the past
|
||||
moveCreationDate?: number
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,7 @@
|
||||
|
||||
export { creations } from 'database'
|
||||
|
||||
/*
|
||||
import { nMonthsBefore } from '@/seeds/factory/creation'
|
||||
|
||||
import { CreationInterface } from './CreationInterface'
|
||||
@ -153,3 +157,4 @@ export const creations: CreationInterface[] = [
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
*/
|
||||
@ -1,32 +1,15 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import {
|
||||
contributionLinkFactory as contributionLinkFactoryDb,
|
||||
ContributionLinkInterface
|
||||
} from 'database'
|
||||
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
|
||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||
import { createContributionLink, login } from '@/seeds/graphql/mutations'
|
||||
export { ContributionLinkInterface }
|
||||
|
||||
export const contributionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
export async function contributionLinkFactory (
|
||||
_client: any,
|
||||
contributionLink: ContributionLinkInterface,
|
||||
): Promise<ContributionLink> => {
|
||||
const { mutate } = client
|
||||
|
||||
// login as admin
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
const variables = {
|
||||
amount: contributionLink.amount,
|
||||
memo: contributionLink.memo,
|
||||
name: contributionLink.name,
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: 200,
|
||||
validFrom: contributionLink.validFrom ? contributionLink.validFrom.toISOString() : undefined,
|
||||
validTo: contributionLink.validTo ? contributionLink.validTo.toISOString() : undefined,
|
||||
}
|
||||
|
||||
const result = await mutate({ mutation: createContributionLink, variables })
|
||||
return result.data.createContributionLink
|
||||
): Promise<ContributionLink> {
|
||||
return new ContributionLink(await contributionLinkFactoryDb(contributionLink))
|
||||
}
|
||||
|
||||
@ -1,58 +1,15 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Contribution, Transaction } from 'database'
|
||||
import {
|
||||
Contribution,
|
||||
creationFactory as creationFactoryDb,
|
||||
CreationInterface,
|
||||
nMonthsBefore
|
||||
} from 'database'
|
||||
|
||||
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { confirmContribution, createContribution, login } from '@/seeds/graphql/mutations'
|
||||
|
||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
return new Date(date.getFullYear(), date.getMonth() - months, 1).toISOString()
|
||||
}
|
||||
export { CreationInterface, nMonthsBefore }
|
||||
|
||||
export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
_client: any,
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution> => {
|
||||
const { mutate } = client
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: creation.email, password: 'Aa12345_' },
|
||||
})
|
||||
const {
|
||||
data: { createContribution: contribution },
|
||||
} = await mutate({ mutation: createContribution, variables: { ...creation } })
|
||||
|
||||
if (creation.confirmed) {
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
await mutate({ mutation: confirmContribution, variables: { id: contribution.id } })
|
||||
const confirmedContribution = await Contribution.findOneOrFail({
|
||||
where: { id: contribution.id },
|
||||
})
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.contributionDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedContribution.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.creationDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.balanceDate = new Date(
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
await transaction.save()
|
||||
await confirmedContribution.save()
|
||||
}
|
||||
}
|
||||
return confirmedContribution
|
||||
} else {
|
||||
return contribution
|
||||
}
|
||||
return creationFactoryDb(creation)
|
||||
}
|
||||
|
||||
@ -1,46 +1,13 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { TransactionLink } from 'database'
|
||||
import {
|
||||
transactionLinkFactory as transactionLinkFactoryDb,
|
||||
TransactionLinkInterface
|
||||
} from 'database'
|
||||
|
||||
import { transactionLinkExpireDate } from '@/graphql/resolver/TransactionLinkResolver'
|
||||
import { createTransactionLink, login } from '@/seeds/graphql/mutations'
|
||||
import { TransactionLinkInterface } from '@/seeds/transactionLink/TransactionLinkInterface'
|
||||
export { TransactionLinkInterface }
|
||||
|
||||
export const transactionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
export async function transactionLinkFactory (
|
||||
_client: any,
|
||||
transactionLink: TransactionLinkInterface,
|
||||
): Promise<void> => {
|
||||
const { mutate } = client
|
||||
|
||||
// login
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: transactionLink.email, password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
const variables = {
|
||||
amount: transactionLink.amount,
|
||||
memo: transactionLink.memo,
|
||||
}
|
||||
|
||||
// get the transaction links's id
|
||||
const {
|
||||
data: {
|
||||
createTransactionLink: { id },
|
||||
},
|
||||
} = await mutate({ mutation: createTransactionLink, variables })
|
||||
|
||||
if (transactionLink.createdAt || transactionLink.deletedAt) {
|
||||
const dbTransactionLink = await TransactionLink.findOneOrFail({ where: { id } })
|
||||
|
||||
if (transactionLink.createdAt) {
|
||||
dbTransactionLink.createdAt = transactionLink.createdAt
|
||||
dbTransactionLink.validUntil = transactionLinkExpireDate(transactionLink.createdAt)
|
||||
await dbTransactionLink.save()
|
||||
}
|
||||
|
||||
if (transactionLink.deletedAt) {
|
||||
dbTransactionLink.deletedAt = new Date()
|
||||
await dbTransactionLink.save()
|
||||
}
|
||||
}
|
||||
): Promise<void> {
|
||||
await transactionLinkFactoryDb(transactionLink)
|
||||
}
|
||||
|
||||
@ -1,74 +1,35 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { User } from 'database'
|
||||
import { User, userFactory as userFactoryDb, userFactoryBulk as userFactoryBulkDb, Community } from 'database'
|
||||
|
||||
import { RoleNames } from '@enum/RoleNames'
|
||||
|
||||
import { setUserRole } from '@/graphql/resolver/util/modifyUserRole'
|
||||
import { writeHomeCommunityEntry } from '@/seeds/community'
|
||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||
import { encryptPassword } from '@/password/PasswordEncryptor'
|
||||
|
||||
export const userFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
_client: any,
|
||||
user: UserInterface,
|
||||
): Promise<User> => {
|
||||
const { mutate } = client
|
||||
|
||||
const homeCom = await writeHomeCommunityEntry()
|
||||
// console.log('call createUser with', JSON.stringify(user, null, 2))
|
||||
const response = await mutate({ mutation: createUser, variables: user })
|
||||
if (!response?.data?.createUser) {
|
||||
// biome-ignore lint/suspicious/noConsole: will be used in tests where logging is mocked
|
||||
// console.log(JSON.stringify(response, null, 2))
|
||||
throw new Error('createUser mutation returned unexpected response')
|
||||
}
|
||||
const {
|
||||
data: {
|
||||
createUser: { id },
|
||||
},
|
||||
} = response
|
||||
// get user from database
|
||||
let dbUser = await User.findOneOrFail({ where: { id }, relations: ['emailContact', 'userRoles'] })
|
||||
|
||||
const emailContact = dbUser.emailContact
|
||||
const dbUser = await userFactoryDb(user, homeCom)
|
||||
|
||||
if (user.emailChecked) {
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode },
|
||||
})
|
||||
}
|
||||
|
||||
// get last changes of user from database
|
||||
dbUser = await User.findOneOrFail({ where: { id }, relations: { userRoles: true, emailContact: true } })
|
||||
|
||||
if (user.createdAt || user.deletedAt || user.role) {
|
||||
if (user.createdAt) {
|
||||
dbUser.createdAt = user.createdAt
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
dbUser.deletedAt = user.deletedAt
|
||||
}
|
||||
const userRole = user.role as RoleNames
|
||||
if (userRole && (userRole === RoleNames.ADMIN || userRole === RoleNames.MODERATOR)) {
|
||||
await setUserRole(dbUser, user.role)
|
||||
}
|
||||
const passwortHash = await encryptPassword(dbUser, 'Aa12345_')
|
||||
dbUser.password = passwortHash
|
||||
await dbUser.save()
|
||||
}
|
||||
try {
|
||||
if (homeCom.communityUuid) {
|
||||
dbUser.communityUuid = homeCom.communityUuid
|
||||
await User.save(dbUser)
|
||||
}
|
||||
} catch (_err) {
|
||||
// no homeCommunity exists
|
||||
}
|
||||
|
||||
// get last changes of user from database
|
||||
dbUser = await User.findOneOrFail({
|
||||
where: { id },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact', 'userRoles'],
|
||||
})
|
||||
return dbUser
|
||||
}
|
||||
|
||||
export async function userFactoryBulk(users: UserInterface[], homeCommunity?: Community | null) {
|
||||
if (!homeCommunity) {
|
||||
homeCommunity = await writeHomeCommunityEntry()
|
||||
}
|
||||
const dbUsers = await userFactoryBulkDb(users, homeCommunity)
|
||||
for (const dbUser of dbUsers) {
|
||||
if (dbUser.emailContact.emailChecked) {
|
||||
const passwortHash = await encryptPassword(dbUser, 'Aa12345_')
|
||||
dbUser.password = passwortHash
|
||||
await dbUser.save()
|
||||
}
|
||||
}
|
||||
return dbUsers
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { entities } from 'database'
|
||||
import { datatype, internet, name } from 'faker'
|
||||
import {
|
||||
AppDatabase,
|
||||
User,
|
||||
UserInterface,
|
||||
creationFactoryBulk,
|
||||
transactionLinkFactoryBulk
|
||||
} from 'database'
|
||||
import { internet, name } from 'faker'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { createServer } from '@/server/createServer'
|
||||
|
||||
import { initLogging } from '@/server/logger'
|
||||
import { getLogger } from 'log4js'
|
||||
@ -11,95 +15,87 @@ import { writeHomeCommunityEntry } from './community'
|
||||
import { contributionLinks } from './contributionLink/index'
|
||||
import { creations } from './creation/index'
|
||||
import { contributionLinkFactory } from './factory/contributionLink'
|
||||
import { creationFactory } from './factory/creation'
|
||||
import { transactionLinkFactory } from './factory/transactionLink'
|
||||
import { userFactory } from './factory/user'
|
||||
import { userFactoryBulk } from './factory/user'
|
||||
import { transactionLinks } from './transactionLink/index'
|
||||
import { users } from './users/index'
|
||||
|
||||
CONFIG.EMAIL = false
|
||||
const RANDOM_USER_COUNT = 100
|
||||
const logger = getLogger('seed')
|
||||
|
||||
const context = {
|
||||
token: '',
|
||||
setHeaders: {
|
||||
push: (value: { key: string; value: string }): void => {
|
||||
context.token = value.value
|
||||
},
|
||||
|
||||
forEach: (): void => {
|
||||
// do nothing
|
||||
},
|
||||
},
|
||||
clientTimezoneOffset: 0,
|
||||
}
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as long we do not have foreign key constraints
|
||||
for (const entity of entities) {
|
||||
if (entity.name !== 'Migration') {
|
||||
await resetEntity(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetEntity = async (entity: any) => {
|
||||
const items = await entity.find({ withDeleted: true })
|
||||
if (items.length > 0) {
|
||||
const ids = items.map((e: any) => e.id)
|
||||
await entity.delete(ids)
|
||||
}
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
initLogging()
|
||||
const server = await createServer(getLogger('apollo'), context)
|
||||
const seedClient = createTestClient(server.apollo)
|
||||
const { con } = server
|
||||
await cleanDB()
|
||||
logger.info('##seed## clean database successful...')
|
||||
const db = AppDatabase.getInstance()
|
||||
await db.init()
|
||||
await clearDatabase(db)
|
||||
logger.info('clean database successful...')
|
||||
logger.info(`crypto worker enabled: ${CONFIG.USE_CRYPTO_WORKER}`)
|
||||
|
||||
// seed home community
|
||||
await writeHomeCommunityEntry()
|
||||
const homeCommunity = await writeHomeCommunityEntry()
|
||||
|
||||
// seed the standard users
|
||||
for (const user of users) {
|
||||
await userFactory(seedClient, user)
|
||||
// put into map for later direct access
|
||||
const userCreationIndexedByEmail = new Map<string, User>()
|
||||
const defaultUsers = await userFactoryBulk(users, homeCommunity)
|
||||
for (const dbUser of defaultUsers) {
|
||||
userCreationIndexedByEmail.set(dbUser.emailContact.email, dbUser)
|
||||
}
|
||||
logger.info('##seed## seeding all standard users successful...')
|
||||
logger.info('seeding all standard users successful...')
|
||||
|
||||
// seed 100 random users
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await userFactory(seedClient, {
|
||||
const randomUsers = new Array<UserInterface>(RANDOM_USER_COUNT)
|
||||
for (let i = 0; i < RANDOM_USER_COUNT; i++) {
|
||||
randomUsers[i] = {
|
||||
firstName: name.firstName(),
|
||||
lastName: name.lastName(),
|
||||
email: internet.email(),
|
||||
language: datatype.boolean() ? 'en' : 'de',
|
||||
})
|
||||
logger.info(`##seed## seed ${i}. random user`)
|
||||
language: Math.random() < 0.5 ? 'en' : 'de',
|
||||
}
|
||||
logger.info('##seed## seeding all random users successful...')
|
||||
}
|
||||
await userFactoryBulk(randomUsers, homeCommunity)
|
||||
logger.info('seeding all random users successful...')
|
||||
|
||||
// create GDD
|
||||
for (const creation of creations) {
|
||||
await creationFactory(seedClient, creation)
|
||||
}
|
||||
logger.info('##seed## seeding all creations successful...')
|
||||
const moderatorUser = userCreationIndexedByEmail.get('peter@lustig.de')!
|
||||
await creationFactoryBulk(creations, userCreationIndexedByEmail, moderatorUser)
|
||||
logger.info('seeding all creations successful...')
|
||||
|
||||
// create Transaction Links
|
||||
for (const transactionLink of transactionLinks) {
|
||||
await transactionLinkFactory(seedClient, transactionLink)
|
||||
const movedTransactionLinks = transactionLinks.map(transactionLink => {
|
||||
let createdAt = new Date(new Date().getTime() + 1000)
|
||||
if (transactionLink.createdAt) {
|
||||
createdAt = transactionLink.createdAt
|
||||
}
|
||||
logger.info('##seed## seeding all transactionLinks successful...')
|
||||
return {
|
||||
...transactionLink,
|
||||
createdAt: createdAt,
|
||||
}
|
||||
})
|
||||
await transactionLinkFactoryBulk(movedTransactionLinks, userCreationIndexedByEmail)
|
||||
logger.info('seeding all transactionLinks successful...')
|
||||
|
||||
// create Contribution Links
|
||||
for (const contributionLink of contributionLinks) {
|
||||
await contributionLinkFactory(seedClient, contributionLink)
|
||||
await contributionLinkFactory(null, contributionLink)
|
||||
}
|
||||
logger.info('##seed## seeding all contributionLinks successful...')
|
||||
logger.info('seeding all contributionLinks successful...')
|
||||
|
||||
await con.destroy()
|
||||
await db.destroy()
|
||||
}
|
||||
|
||||
async function clearDatabase(db: AppDatabase) {
|
||||
await db.getDataSource().transaction(async trx => {
|
||||
await trx.query(`SET FOREIGN_KEY_CHECKS = 0`)
|
||||
await trx.query(`TRUNCATE TABLE contributions`)
|
||||
await trx.query(`TRUNCATE TABLE contribution_links`)
|
||||
await trx.query(`TRUNCATE TABLE users`)
|
||||
await trx.query(`TRUNCATE TABLE user_contacts`)
|
||||
await trx.query(`TRUNCATE TABLE user_roles`)
|
||||
await trx.query(`TRUNCATE TABLE transactions`)
|
||||
await trx.query(`TRUNCATE TABLE transaction_links`)
|
||||
await trx.query(`TRUNCATE TABLE communities`)
|
||||
await trx.query(`SET FOREIGN_KEY_CHECKS = 1`)
|
||||
})
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
export { TransactionLinkInterface } from 'database'
|
||||
/*
|
||||
export interface TransactionLinkInterface {
|
||||
email: string
|
||||
amount: number
|
||||
@ -8,3 +10,4 @@ export interface TransactionLinkInterface {
|
||||
// redeemedBy?: number
|
||||
deletedAt?: boolean
|
||||
}
|
||||
*/
|
||||
@ -1,11 +1,12 @@
|
||||
export { transactionLinks } from 'database'
|
||||
/*
|
||||
import { TransactionLinkInterface } from './TransactionLinkInterface'
|
||||
|
||||
export const transactionLinks: TransactionLinkInterface[] = [
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 19.99,
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: new Date(2022, 0, 1),
|
||||
memo: 'Leider wollte niemand meine Gradidos haben :(',
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
@ -54,3 +55,4 @@ bei Gradidio sei dabei!`,
|
||||
deletedAt: true,
|
||||
},
|
||||
]
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { UserInterface } from 'database'
|
||||
/*
|
||||
export interface UserInterface {
|
||||
alias?: string
|
||||
email?: string
|
||||
@ -11,3 +13,4 @@ export interface UserInterface {
|
||||
publisherId?: number
|
||||
role?: string
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { bibiBloxberg } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const bibiBloxberg: UserInterface = {
|
||||
@ -9,4 +11,7 @@ export const bibiBloxberg: UserInterface = {
|
||||
emailChecked: true,
|
||||
language: 'de',
|
||||
publisherId: 1234,
|
||||
// move user createdAt before transaction link
|
||||
createdAt: new Date(2021, 9, 17),
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { bobBaumeister } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const bobBaumeister: UserInterface = {
|
||||
@ -9,3 +11,4 @@ export const bobBaumeister: UserInterface = {
|
||||
emailChecked: true,
|
||||
language: 'de',
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,6 @@
|
||||
|
||||
export { garrickOllivander } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const garrickOllivander: UserInterface = {
|
||||
@ -10,3 +13,4 @@ export const garrickOllivander: UserInterface = {
|
||||
emailChecked: false,
|
||||
language: 'en',
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { peterLustig } from 'database'
|
||||
/*
|
||||
import { RoleNames } from '@enum/RoleNames'
|
||||
|
||||
import { UserInterface } from './UserInterface'
|
||||
@ -12,3 +14,4 @@ export const peterLustig: UserInterface = {
|
||||
language: 'de',
|
||||
role: RoleNames.ADMIN,
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,6 @@
|
||||
|
||||
export { raeuberHotzenplotz } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const raeuberHotzenplotz: UserInterface = {
|
||||
@ -8,3 +11,4 @@ export const raeuberHotzenplotz: UserInterface = {
|
||||
emailChecked: true,
|
||||
language: 'de',
|
||||
}
|
||||
*/
|
||||
@ -1,3 +1,5 @@
|
||||
export { stephenHawking } from 'database'
|
||||
/*
|
||||
import { UserInterface } from './UserInterface'
|
||||
|
||||
export const stephenHawking: UserInterface = {
|
||||
@ -10,3 +12,4 @@ export const stephenHawking: UserInterface = {
|
||||
deletedAt: new Date('2018-03-14T09:17:52'),
|
||||
language: 'en',
|
||||
}
|
||||
*/
|
||||
@ -1,4 +1,5 @@
|
||||
import { CONFIG } from '@/config'
|
||||
import { CONFIG as CORE_CONFIG } from 'core'
|
||||
import { schema } from '@/graphql/schema'
|
||||
import { elopageWebhook } from '@/webhook/elopage'
|
||||
import { gmsWebhook } from '@/webhook/gms'
|
||||
@ -8,12 +9,10 @@ import { slowDown } from 'express-slow-down'
|
||||
import helmet from 'helmet'
|
||||
import { Logger, getLogger } from 'log4js'
|
||||
import { DataSource } from 'typeorm'
|
||||
|
||||
import { GRADIDO_REALM, LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { AppDatabase } from 'database'
|
||||
import { context as serverContext } from './context'
|
||||
import { cors } from './cors'
|
||||
import { i18n } from './localization'
|
||||
import { plugins } from './plugins'
|
||||
import { jwks, openidConfiguration } from '@/openIDConnect'
|
||||
// TODO implement
|
||||
@ -23,12 +22,12 @@ interface ServerDef {
|
||||
apollo: ApolloServer
|
||||
app: Express
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
export const createServer = async (
|
||||
apolloLogger: Logger,
|
||||
context: any = serverContext,
|
||||
localization: i18n.I18n = i18n,
|
||||
): Promise<ServerDef> => {
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.createServer`)
|
||||
logger.debug('createServer...')
|
||||
@ -74,9 +73,6 @@ export const createServer = async (
|
||||
// bodyparser urlencoded for elopage
|
||||
app.use(urlencoded({ extended: true }))
|
||||
|
||||
// i18n
|
||||
app.use(localization.init)
|
||||
|
||||
// Elopage Webhook
|
||||
|
||||
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||
@ -100,9 +96,9 @@ export const createServer = async (
|
||||
})
|
||||
apollo.applyMiddleware({ app, path: '/' })
|
||||
logger.info(
|
||||
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CORE_CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CORE_CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||
)
|
||||
logger.debug('createServer...successful')
|
||||
|
||||
return { apollo, app, con: db.getDataSource() }
|
||||
return { apollo, app, con: db.getDataSource(), db }
|
||||
}
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import path from 'node:path'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import i18n from 'i18n'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.server.localization`)
|
||||
|
||||
i18n.configure({
|
||||
locales: ['en', 'de'],
|
||||
defaultLocale: 'en',
|
||||
retryInDefaultLocale: false,
|
||||
directory: path.join(__dirname, '..', 'locales'),
|
||||
// autoReload: true, // if this is activated the seeding hangs at the very end
|
||||
updateFiles: false,
|
||||
objectNotation: true,
|
||||
logDebugFn: (msg) => logger.debug(msg),
|
||||
logWarnFn: (msg) => logger.info(msg),
|
||||
logErrorFn: (msg) => logger.error(msg),
|
||||
// this api is needed for email-template pug files
|
||||
api: {
|
||||
__: 't', // now req.__ becomes req.t
|
||||
__n: 'tn', // and req.__n can be called as req.tn
|
||||
},
|
||||
register: global,
|
||||
mustacheConfig: {
|
||||
tags: ['{', '}'],
|
||||
disable: false,
|
||||
},
|
||||
})
|
||||
|
||||
export { i18n }
|
||||
@ -1,6 +1,7 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Event as DbEvent } from 'database'
|
||||
import { DataSource } from 'typeorm'
|
||||
import { AppDatabase } from 'database'
|
||||
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
|
||||
@ -19,22 +20,26 @@ jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate']
|
||||
let con: DataSource
|
||||
let db: AppDatabase
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
query: ApolloServerTestClient['query']
|
||||
con: DataSource
|
||||
db: AppDatabase
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
db = testEnv.db
|
||||
await DbEvent.clear()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.destroy()
|
||||
await db.getRedisClient().quit()
|
||||
})
|
||||
|
||||
describe('klicktipp', () => {
|
||||
|
||||
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