Merge branch 'master' into fix_small_bugs

This commit is contained in:
einhornimmond 2025-12-04 09:54:52 +01:00
commit c48b7c45d9
551 changed files with 11077 additions and 21496 deletions

View File

@ -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

View File

@ -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
@ -92,19 +92,4 @@ jobs:
bun install --global --no-save turbo@^2
- 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
run: turbo backend#typecheck backend#build

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -28,17 +28,20 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: create .env
run: |
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: Build 'test' image
run: |
docker build --target test -t "gradido/dlt-connector:test" -f dlt-connector/Dockerfile .
docker save "gradido/dlt-connector:test" > /tmp/dlt-connector.tar
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: docker-dlt-connector-test
path: /tmp/dlt-connector.tar
run: docker build --target production -t "gradido/dlt-connector:productionTest" -f dlt-connector/Dockerfile .
unit_test:
name: Unit Tests - DLT Connector
@ -48,13 +51,21 @@ jobs:
steps:
- 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

View File

@ -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

View File

@ -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
View File

@ -1 +1,4 @@
[submodule "inspector"]
path = inspector
url = https://github.com/gradido/inspector.git

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -26,6 +26,7 @@ export const searchUsers = gql`
hasElopage
emailConfirmationSend
deletedAt
createdAt
roles
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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' },

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,5 +1,6 @@
# Server
JWT_EXPIRES_IN=2m
BACKEND_PORT=4000
GDT_ACTIVE=false
HUMHUB_ACTIVE=false

View File

@ -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"]

View File

@ -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": "*",

View File

@ -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)

View File

@ -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)
})
})
*/

View File

@ -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,
)
if (error) {
throw new Error(error.message)
}
return succeed
} catch (e) {
throw new LogError('Error send sending transaction to dlt-connector: ', e)
}
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
)
}
}

View File

@ -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: {},
})
}
}

View 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
}

View 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,
}

View File

@ -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',
}

View File

@ -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
})

View 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)
}

View 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
}
}

View File

@ -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
}
}

View 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
}
}

View File

@ -1,7 +0,0 @@
import { TransactionErrorType } from '@dltConnector/enum/TransactionErrorType'
export interface TransactionError {
type: TransactionErrorType
message: string
name: string
}

View File

@ -1,8 +0,0 @@
import { TransactionType } from '@dltConnector/enum/TransactionType'
export interface TransactionRecipe {
id: number
createdAt: string
type: TransactionType
topic: string
}

View File

@ -1,8 +0,0 @@
import { TransactionError } from './TransactionError'
import { TransactionRecipe } from './TransactionRecipe'
export interface TransactionResult {
error?: TransactionError
recipe?: TransactionRecipe
succeed: boolean
}

View File

@ -1,5 +0,0 @@
export interface UserIdentifier {
uuid: string
communityUuid: string
accountNr?: number
}

View File

@ -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')

View File

@ -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 []
}

View File

@ -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

View File

@ -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 => {
@ -209,7 +125,7 @@ export const schema = Joi.object({
.description('Time in minutes before a new code can be requested')
.required(),
FEDERATION_VALIDATE_COMMUNITY_TIMER: Joi.number()
FEDERATION_VALIDATE_COMMUNITY_TIMER: Joi.number()
.integer()
.min(1000)
.default(60000)
@ -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)

View File

@ -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,
},
})
}

View File

@ -4,4 +4,5 @@ export interface PublicCommunityInfo {
creationDate: Date
publicKey: string
publicJwtKey: string
hieroTopicId: string | null
}

View File

@ -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', () => {

View File

@ -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)
}
}

View File

@ -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

View File

@ -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',

View File

@ -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',

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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,

View File

@ -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()

View File

@ -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,
@ -469,12 +468,10 @@ export class ContributionResolver {
contribution.contributionDate,
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
@ -518,10 +515,7 @@ export class ContributionResolver {
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
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
}

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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({

View File

@ -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()
}
// trigger to send transaction via dlt-connector
await sendTransactionsToDltConnector()
// releaseLock()
await mutex.release()
}
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
}

View File

@ -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 () => {

View File

@ -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)
@ -71,8 +84,7 @@ export const executeTransaction = async (
throw new LogError('Sender and Recipient are the same', sender.id)
}
// validate amount
const receivedCallDate = new Date()
// validate amount
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
}

View File

@ -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',

View File

@ -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
@ -398,8 +396,11 @@ export class UserResolver {
dbUser.alias = alias
}
dbUser.publisherId = publisherId ?? 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser', new UserLoggingView(dbUser))
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,

View File

@ -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

View File

@ -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 = ''

View File

@ -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 => {

View File

@ -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',
)
})
})
*/
})
})

View File

@ -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)
}
}

View 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}`
},
},
})
}
}

View File

@ -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))

View File

@ -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
}
*/

View File

@ -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),
},
]
*/

View File

@ -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
}
*/

View File

@ -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,
},
]
*/

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
logger.info('##seed## seeding all transactionLinks successful...')
const movedTransactionLinks = transactionLinks.map(transactionLink => {
let createdAt = new Date(new Date().getTime() + 1000)
if (transactionLink.createdAt) {
createdAt = transactionLink.createdAt
}
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) => {

View File

@ -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
}
*/

View File

@ -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,
},
]
*/

View File

@ -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
}
*/

View File

@ -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),
}
*/

View File

@ -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',
}
*/

View File

@ -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',
}
*/

View File

@ -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,
}
*/

View File

@ -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',
}
*/

View File

@ -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',
}
*/

View File

@ -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 }
}

View File

@ -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 }

View File

@ -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