Merge branch 'master' into eslint-plugin-import-fixes

This commit is contained in:
Ulf Gebhardt 2023-03-28 01:08:16 +02:00
commit c874fb6ca1
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
125 changed files with 2892 additions and 1978 deletions

46
.github/file-filters.yml vendored Normal file
View File

@ -0,0 +1,46 @@
# These file filter patterns are used by the action https://github.com/dorny/paths-filter
# more differentiated filters for admin interface, which might be used later
# admin_locales: &admin_locales
# - 'admin/src/locales/**'
# - 'admin/scripts/sort*'
# admin_stylelinting: &admin_stylelinting
# - 'admin/{components,layouts,pages}/**/*.{scss,vue}'
# - 'admin/.stylelintrc.js'
# admin_linting: &admin_linting
# - 'admin/.eslint*'
# - 'admin/babel.config.js'
# - 'admin/package.json'
# - 'admin/**/*.{js,vue}'
# - *admin_locales
# admin_unit_testing: &admin_unit_testing
# - 'admin/package.json'
# - 'admin/{jest,vue}.config.js'
# - 'admin/{public,run,test}/**/*'
# - 'admin/src/!(locales)/**/*'
# admin_docker_building: &admin_docker_building
# - 'admin/.dockerignore'
# - 'admin/Dockerfile'
# - *admin_unit_testing
admin: &admin
- 'admin/**/*'
dht_node: &dht_node
- 'dht-node/**/*'
docker: &docker
- 'docker-compose.*'
federation: &federation
- 'federation/**/*'
frontend: &frontend
- 'frontend/**/*'
nginx: &nginx
- 'nginx/**/*'

58
.github/workflows/e2e-test.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Gradido End-to-End Test CI
on: push
jobs:
end-to-end-tests:
name: End-to-End Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Boot up test system | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
- name: Boot up test system | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend
run: |
cd backend
cp .env.test_e2e .env
cd ..
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
- name: Sleep for 10 seconds
run: sleep 10s
- name: Boot up test system | seed backend
run: |
sudo chown runner:docker -R *
cd database
yarn && yarn dev_reset
cd ../backend
yarn && yarn seed
cd ..
- name: Boot up test system | docker-compose frontends
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
- name: Boot up test system | docker-compose mailserver
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
- name: Sleep for 15 seconds
run: sleep 15s
- name: End-to-end tests | run tests
id: e2e-tests
run: |
cd e2e-tests/
yarn
yarn run cypress run
- name: End-to-end tests | if tests failed, upload screenshots
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/

View File

@ -0,0 +1,84 @@
name: Gradido Admin Interface Test CI
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - Admin Interface
runs-on: ubuntu-latest
outputs:
admin: ${{ steps.changes.outputs.admin }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for admin interface file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test:
if: needs.files-changed.outputs.admin == 'true'
name: Docker Build Test - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Build 'test' image
run: docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
unit_test:
if: needs.files-changed.outputs.admin == 'true'
name: Unit Tests - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Unit tests
run: cd admin && yarn && yarn run test
lint:
if: needs.files-changed.outputs.admin == 'true'
name: Lint - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Lint
run: cd admin && yarn && yarn run lint
stylelint:
if: needs.files-changed.outputs.admin == 'true'
name: Stylelint - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Stylelint
run: cd admin && yarn && yarn run stylelint
locales:
if: needs.files-changed.outputs.admin == 'true'
name: Locales - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Locales
run: cd admin && yarn && yarn run locales

32
.github/workflows/test-nginx.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Gradido Nginx Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Nginx
runs-on: ubuntu-latest
outputs:
nginx: ${{ steps.changes.outputs.nginx }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for nginx file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test_nginx:
name: Docker Build Test - Nginx
if: needs.files-changed.outputs.nginx == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: nginx | Build 'test' image
run: docker build -t "gradido/nginx:test" nginx/

View File

@ -3,57 +3,6 @@ name: gradido test CI
on: push on: push
jobs: jobs:
##############################################################################
# JOB: DOCKER BUILD TEST FRONTEND ############################################
##############################################################################
build_test_frontend:
name: Docker Build Test - Frontend
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# FRONTEND ###############################################################
##########################################################################
- name: Frontend | Build `test` image
run: |
docker build --target test -t "gradido/frontend:test" frontend/
docker save "gradido/frontend:test" > /tmp/frontend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-frontend-test
path: /tmp/frontend.tar
##############################################################################
# JOB: DOCKER BUILD TEST ADMIN INTERFACE #####################################
##############################################################################
build_test_admin:
name: Docker Build Test - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# ADMIN INTERFACE ########################################################
##########################################################################
- name: Admin | Build `test` image
run: |
docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
docker save "gradido/admin:test" > /tmp/admin.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-admin-test
path: /tmp/admin.tar
############################################################################## ##############################################################################
# JOB: DOCKER BUILD TEST BACKEND ############################################# # JOB: DOCKER BUILD TEST BACKEND #############################################
############################################################################## ##############################################################################
@ -132,139 +81,6 @@ jobs:
name: docker-mariadb-test name: docker-mariadb-test
path: /tmp/mariadb.tar path: /tmp/mariadb.tar
##############################################################################
# JOB: DOCKER BUILD TEST NGINX ###############################################
##############################################################################
build_test_nginx:
name: Docker Build Test - Nginx
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BUILD NGINX DOCKER IMAGE ###############################################
##########################################################################
- name: nginx | Build `test` image
run: |
docker build -t "gradido/nginx:test" nginx/
docker save "gradido/nginx:test" > /tmp/nginx.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-nginx-test
path: /tmp/nginx.tar
##############################################################################
# JOB: LOCALES FRONTEND ######################################################
##############################################################################
locales_frontend:
name: Locales - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Frontend | Locales
run: cd frontend && yarn && yarn run locales
##############################################################################
# JOB: LINT FRONTEND #########################################################
##############################################################################
lint_frontend:
name: Lint - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: Frontend | Lint
run: cd frontend && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
##############################################################################
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: cd frontend && yarn && yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
##############################################################################
lint_admin:
name: Lint - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LINT ADMIN INTERFACE ###################################################
##########################################################################
- name: Admin Interface | Lint
run: cd admin && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT ADMIN INTERFACE #############################################
##############################################################################
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: cd admin && yarn && yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
##############################################################################
locales_admin:
name: Locales - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Admin | Locales
run: cd admin && yarn && yarn run locales
############################################################################## ##############################################################################
# JOB: LINT BACKEND ########################################################## # JOB: LINT BACKEND ##########################################################
############################################################################## ##############################################################################
@ -319,68 +135,6 @@ jobs:
- name: Database | Lint - name: Database | Lint
run: cd database && yarn && yarn run lint run: cd database && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST FRONTEND ###################################################
##############################################################################
unit_test_frontend:
name: Unit tests - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# UNIT TESTS FRONTEND ####################################################
##########################################################################
- name: Frontend | Unit tests
run: |
cd frontend && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK FRONTEND ################################################
##########################################################################
- name: frontend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Frontend
type: lcov
result_path: ./frontend/coverage/lcov.info
min_coverage: 95
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST ADMIN INTERFACE #############################################
##############################################################################
unit_test_admin:
name: Unit tests - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# UNIT TESTS ADMIN INTERFACE #############################################
##########################################################################
- name: Admin Interface | Unit tests
run: |
cd admin && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK ADMIN INTERFACE #########################################
##########################################################################
- name: Admin Interface | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Admin Interface
type: lcov
result_path: ./admin/coverage/lcov.info
min_coverage: 97
token: ${{ github.token }}
############################################################################## ##############################################################################
# JOB: UNIT TEST BACKEND #################################################### # JOB: UNIT TEST BACKEND ####################################################
############################################################################## ##############################################################################
@ -415,20 +169,7 @@ jobs:
- name: backend | docker-compose database - name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test - name: backend Unit tests | test
run: | run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK BACKEND #################################################
##########################################################################
- name: backend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 80
token: ${{ github.token }}
########################################################################## ##########################################################################
# DATABASE MIGRATION TEST UP + RESET ##################################### # DATABASE MIGRATION TEST UP + RESET #####################################
@ -452,108 +193,3 @@ jobs:
run: docker-compose -f docker-compose.yml run -T database yarn up run: docker-compose -f docker-compose.yml run -T database yarn up
- name: database | reset - name: database | reset
run: docker-compose -f docker-compose.yml run -T database yarn reset run: docker-compose -f docker-compose.yml run -T database yarn reset
##############################################################################
# JOB: END-TO-END TESTS #####################################################
##############################################################################
end-to-end-tests:
name: End-to-End Tests
runs-on: ubuntu-latest
needs: [build_test_mariadb, build_test_database_up, build_test_admin, build_test_frontend, build_test_nginx]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Mariadb)
uses: actions/download-artifact@v3
with:
name: docker-mariadb-test
path: /tmp
- name: Load Docker Image (Mariadb)
run: docker load < /tmp/mariadb.tar
- name: Download Docker Image (Database Up)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image (Database Up)
run: docker load < /tmp/database_up.tar
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image (Frontend)
run: docker load < /tmp/frontend.tar
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image (Admin Interface)
run: docker load < /tmp/admin.tar
- name: Download Docker Image (Nginx)
uses: actions/download-artifact@v3
with:
name: docker-nginx-test
path: /tmp
- name: Load Docker Image (Nginx)
run: docker load < /tmp/nginx.tar
##########################################################################
# BOOT UP THE TEST SYSTEM ################################################
##########################################################################
- name: Boot up test system | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
- name: Boot up test system | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend
run: |
cd backend
cp .env.test_e2e .env
cd ..
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
- name: Sleep for 10 seconds
run: sleep 10s
- name: Boot up test system | seed backend
run: |
sudo chown runner:docker -R *
cd database
yarn && yarn dev_reset
cd ../backend
yarn && yarn seed
cd ..
- name: Boot up test system | docker-compose frontends
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
- name: Boot up test system | docker-compose mailserver
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
- name: Sleep for 15 seconds
run: sleep 15s
##########################################################################
# END-TO-END TESTS #######################################################
##########################################################################
- name: End-to-end tests | run tests
id: e2e-tests
run: |
cd e2e-tests/
yarn
yarn run cypress run --spec cypress/e2e/User.Authentication.feature,cypress/e2e/User.Authentication.ResetPassword.feature,cypress/e2e/User.Registration.feature
- name: End-to-end tests | if tests failed, upload screenshots
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/

View File

@ -3,17 +3,38 @@ name: Gradido DHT Node Test CI
on: push on: push
jobs: jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - DHT Node
runs-on: ubuntu-latest
outputs:
dht_node: ${{ steps.changes.outputs.dht_node }}
docker: ${{ steps.changes.outputs.docker }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
############################################################################## ##############################################################################
# JOB: DOCKER BUILD TEST ##################################################### # JOB: DOCKER BUILD TEST #####################################################
############################################################################## ##############################################################################
build: build:
name: Docker Build Test - DHT Node name: Docker Build Test - DHT Node
if: needs.files-changed.outputs.dht_node == 'true' || needs.files-changed.outputs.docker == 'true'
needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Build `test` image - name: Build 'test' image
run: | run: |
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile . docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
docker save "gradido/dht-node:test" > /tmp/dht-node.tar docker save "gradido/dht-node:test" > /tmp/dht-node.tar
@ -29,30 +50,24 @@ jobs:
############################################################################## ##############################################################################
lint: lint:
name: Lint - DHT Node name: Lint - DHT Node
if: needs.files-changed.outputs.dht_node == 'true'
needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-dht-node-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/dht-node.tar
- name: Lint - name: Lint
run: docker run --rm gradido/dht-node:test yarn run lint run: cd dht-node && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: UNIT TEST ############################################################# # JOB: UNIT TEST #############################################################
############################################################################## ##############################################################################
unit_test: unit_test:
name: Unit Tests - DHT Node name: Unit Tests - DHT Node
if: needs.files-changed.outputs.dht_node == 'true' || needs.files-changed.outputs.docker == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -83,16 +98,4 @@ jobs:
#- name: Unit tests #- name: Unit tests
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test # run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests - name: Unit tests
run: | run: docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/dht-node:test yarn run test
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/dht-node:test yarn run test
cp -r ~/coverage ./coverage
- name: Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage DHT Node
type: lcov
#result_path: ./dht-node/coverage/lcov.info
result_path: ./coverage/lcov.info
min_coverage: 79
token: ${{ github.token }}

View File

@ -3,11 +3,32 @@ name: Gradido Federation Test CI
on: push on: push
jobs: jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - Federation
runs-on: ubuntu-latest
outputs:
docker: ${{ steps.changes.outputs.docker }}
federation: ${{ steps.changes.outputs.federation }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
############################################################################## ##############################################################################
# JOB: DOCKER BUILD TEST ##################################################### # JOB: DOCKER BUILD TEST #####################################################
############################################################################## ##############################################################################
build: build:
name: Docker Build Test - Federation name: Docker Build Test - Federation
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.federation == 'true'
needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
@ -29,30 +50,24 @@ jobs:
############################################################################## ##############################################################################
lint: lint:
name: Lint - Federation name: Lint - Federation
if: needs.files-changed.outputs.federation == 'true'
needs: files-changed
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-federation-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/federation.tar
- name: Lint - name: Lint
run: docker run --rm gradido/federation:test yarn run lint run: cd federation && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: UNIT TEST ############################################################# # JOB: UNIT TEST #############################################################
############################################################################## ##############################################################################
unit_test: unit_test:
name: Unit Tests - Federation name: Unit Tests - Federation
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.federation == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -84,15 +99,4 @@ jobs:
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test # run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests - name: Unit tests
run: | run: |
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/federation:test yarn run test docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/federation:test yarn run test
cp -r ~/coverage ./coverage
- name: Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Federation
type: lcov
#result_path: ./federation/coverage/lcov.info
result_path: ./coverage/lcov.info
min_coverage: 72
token: ${{ github.token }}

84
.github/workflows/test_frontend.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Gradido Frontend Test CI
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - Frontend
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test:
if: needs.files-changed.outputs.frontend == 'true'
name: Docker Build Test - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Build 'test' image
run: docker build --target test -t "gradido/frontend:test" frontend/ --build-arg NODE_ENV="test"
unit_test:
if: needs.files-changed.outputs.frontend == 'true'
name: Unit Tests - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Unit tests
run: cd frontend && yarn && yarn run test
lint:
if: needs.files-changed.outputs.frontend == 'true'
name: Lint - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Lint
run: cd frontend && yarn && yarn run lint
stylelint:
if: needs.files-changed.outputs.frontend == 'true'
name: Stylelint - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Stylelint
run: cd frontend && yarn && yarn run stylelint
locales:
if: needs.files-changed.outputs.frontend == 'true'
name: Locales - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Locales
run: cd frontend && yarn && yarn run locales

View File

@ -76,7 +76,11 @@ git clone git@github.com:gradido/gradido.git
git submodule update --recursive --init git submodule update --recursive --init
``` ```
### 2. Run docker-compose ### 2. Install modules
You can go in each under folder (admin, frontend, database, backend, ...) and call ``yarn`` in each folder or you can call ``yarn installAll``.
### 3. Run docker-compose
Run docker-compose to bring up the development environment Run docker-compose to bring up the development environment

View File

@ -1,11 +1,17 @@
module.exports = { module.exports = {
verbose: true, verbose: true,
collectCoverage: true,
collectCoverageFrom: [ collectCoverageFrom: [
'src/**/*.{js,vue}', 'src/**/*.{js,vue}',
'!**/node_modules/**', '!**/node_modules/**',
'!src/assets/**', '!src/assets/**',
'!**/?(*.)+(spec|test).js?(x)', '!**/?(*.)+(spec|test).js?(x)',
], ],
coverageThreshold: {
global: {
lines: 97,
},
},
moduleFileExtensions: [ moduleFileExtensions: [
'js', 'js',
// 'jsx', // 'jsx',

View File

@ -14,7 +14,7 @@
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json", "analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .", "lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'", "stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest --coverage", "test": "cross-env TZ=UTC jest",
"locales": "scripts/sort.sh" "locales": "scripts/sort.sh"
}, },
"dependencies": { "dependencies": {

View File

@ -1,43 +1,75 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import CreationTransactionList from './CreationTransactionList' import CreationTransactionList from './CreationTransactionList'
import { toastErrorSpy } from '../../test/testSetup' import { toastErrorSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminListContributions } from '../graphql/adminListContributions'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue const localVue = global.localVue
localVue.use(VueApollo)
const apolloQueryMock = jest.fn().mockResolvedValue({ const defaultData = () => {
data: { return {
creationTransactionList: { adminListContributions: {
contributionCount: 2, contributionCount: 2,
contributionList: [ contributionList: [
{ {
id: 1, id: 1,
amount: 5.8, firstName: 'Bibi',
createdAt: '2022-09-21T11:09:51.000Z', lastName: 'Bloxberg',
confirmedAt: null, userId: 99,
contributionDate: '2022-08-01T00:00:00.000Z', email: 'bibi@bloxberg.de',
memo: 'für deine Hilfe, Fräulein Rottenmeier', amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING', state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
}, },
{ {
id: 2, id: 2,
amount: '47', firstName: 'Räuber',
createdAt: '2022-09-21T11:09:28.000Z', lastName: 'Hotzenplotz',
confirmedAt: '2022-09-21T11:09:28.000Z', userId: 100,
contributionDate: '2022-08-01T00:00:00.000Z', email: 'raeuber@hotzenplotz.de',
memo: 'für deine Hilfe, Frau Holle', amount: 1000000,
state: 'CONFIRMED', memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: new Date(),
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
}, },
], ],
}, },
}, }
}) }
const mocks = { const mocks = {
$d: jest.fn((t) => t), $d: jest.fn((t) => t),
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},
} }
const propsData = { const propsData = {
@ -48,47 +80,44 @@ const propsData = {
describe('CreationTransactionList', () => { describe('CreationTransactionList', () => {
let wrapper let wrapper
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => { const Wrapper = () => {
return mount(CreationTransactionList, { localVue, mocks, propsData }) return mount(CreationTransactionList, { localVue, mocks, propsData, apolloProvider })
} }
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper() wrapper = Wrapper()
}) })
describe('server error', () => {
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
describe('sever success', () => {
it('sends query to Apollo when created', () => { it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith( expect(adminListContributionsMock).toBeCalledWith({
expect.objectContaining({
variables: {
currentPage: 1, currentPage: 1,
pageSize: 10, pageSize: 10,
order: 'DESC', order: 'DESC',
userId: 1, userId: 1,
}, })
}),
)
}) })
it('has two values for the transaction', () => { it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2) expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
}) })
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
describe('watch currentPage', () => { describe('watch currentPage', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -100,4 +129,5 @@ describe('CreationTransactionList', () => {
}) })
}) })
}) })
})
}) })

View File

@ -42,7 +42,7 @@
</div> </div>
</template> </template>
<script> <script>
import { creationTransactionList } from '../graphql/creationTransactionList' import { adminListContributions } from '../graphql/adminListContributions'
export default { export default {
name: 'CreationTransactionList', name: 'CreationTransactionList',
props: { props: {
@ -92,33 +92,26 @@ export default {
], ],
} }
}, },
methods: { apollo: {
getTransactions() { AdminListContributions: {
this.$apollo query() {
.query({ return adminListContributions
query: creationTransactionList, },
variables: { variables() {
return {
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.perPage, pageSize: this.perPage,
order: 'DESC', order: 'DESC',
userId: parseInt(this.userId), userId: parseInt(this.userId),
}
}, },
}) update({ adminListContributions }) {
.then((result) => { this.rows = adminListContributions.contributionCount
this.rows = result.data.creationTransactionList.contributionCount this.items = adminListContributions.contributionList
this.items = result.data.creationTransactionList.contributionList
})
.catch((error) => {
this.toastError(error.message)
})
}, },
error({ message }) {
this.toastError(message)
}, },
created() {
this.getTransactions()
},
watch: {
currentPage() {
this.getTransactions()
}, },
}, },
} }

View File

@ -1,17 +1,19 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export const adminListAllContributions = gql` export const adminListContributions = gql`
query ( query (
$currentPage: Int = 1 $currentPage: Int = 1
$pageSize: Int = 25 $pageSize: Int = 25
$order: Order = DESC $order: Order = DESC
$statusFilter: [ContributionStatus!] $statusFilter: [ContributionStatus!]
$userId: Int
) { ) {
adminListAllContributions( adminListContributions(
currentPage: $currentPage currentPage: $currentPage
pageSize: $pageSize pageSize: $pageSize
order: $order order: $order
statusFilter: $statusFilter statusFilter: $statusFilter
userId: $userId
) { ) {
contributionCount contributionCount
contributionList { contributionList {

View File

@ -1,23 +0,0 @@
import gql from 'graphql-tag'
export const creationTransactionList = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC, $userId: Int!) {
creationTransactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
userId: $userId
) {
contributionCount
contributionList {
id
amount
createdAt
confirmedAt
contributionDate
memo
state
}
}
}
`

View File

@ -39,7 +39,7 @@
}, },
"created": "Created for", "created": "Created for",
"createdAt": "Created at", "createdAt": "Created at",
"creation": "Amount", "creation": "Creation",
"creationList": "Creation list", "creationList": "Creation list",
"creation_form": { "creation_form": {
"creation_for": "Active Basic Income for", "creation_for": "Active Basic Income for",

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm' import CreationConfirm from './CreationConfirm'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { adminListAllContributions } from '../graphql/adminListAllContributions' import { adminListContributions } from '../graphql/adminListContributions'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
@ -38,7 +38,7 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
adminListAllContributions: { adminListContributions: {
contributionCount: 2, contributionCount: 2,
contributionList: [ contributionList: [
{ {
@ -92,14 +92,14 @@ const defaultData = () => {
describe('CreationConfirm', () => { describe('CreationConfirm', () => {
let wrapper let wrapper
const adminListAllContributionsMock = jest.fn() const adminListContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn() const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn() const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn() const confirmContributionMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
adminListAllContributions, adminListContributions,
adminListAllContributionsMock adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -337,7 +337,7 @@ describe('CreationConfirm', () => {
}) })
it('refetches contributions with proper filter', () => { it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({ expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,
@ -352,7 +352,7 @@ describe('CreationConfirm', () => {
}) })
it('refetches contributions with proper filter', () => { it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({ expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,
@ -368,7 +368,7 @@ describe('CreationConfirm', () => {
}) })
it('refetches contributions with proper filter', () => { it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({ expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,
@ -384,7 +384,7 @@ describe('CreationConfirm', () => {
}) })
it('refetches contributions with proper filter', () => { it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({ expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,
@ -400,7 +400,7 @@ describe('CreationConfirm', () => {
}) })
it('refetches contributions with proper filter', () => { it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({ expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,

View File

@ -85,7 +85,7 @@
<script> <script>
import Overlay from '../components/Overlay' import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable' import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import { adminListAllContributions } from '../graphql/adminListAllContributions' import { adminListContributions } from '../graphql/adminListContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
@ -397,7 +397,7 @@ export default {
apollo: { apollo: {
ListAllContributions: { ListAllContributions: {
query() { query() {
return adminListAllContributions return adminListContributions
}, },
variables() { variables() {
return { return {
@ -407,11 +407,11 @@ export default {
} }
}, },
fetchPolicy: 'no-cache', fetchPolicy: 'no-cache',
update({ adminListAllContributions }) { update({ adminListContributions }) {
this.rows = adminListAllContributions.contributionCount this.rows = adminListContributions.contributionCount
this.items = adminListAllContributions.contributionList this.items = adminListContributions.contributionList
if (this.statusFilter === FILTER_TAB_MAP[0]) { if (this.statusFilter === FILTER_TAB_MAP[0]) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount) this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
} }
}, },
error({ message }) { error({ message }) {

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Overview from './Overview' import Overview from './Overview'
import { adminListAllContributions } from '../graphql/adminListAllContributions' import { adminListContributions } from '../graphql/adminListContributions'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client' import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup' import { toastErrorSpy } from '../../test/testSetup'
@ -30,7 +30,7 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
adminListAllContributions: { adminListContributions: {
contributionCount: 2, contributionCount: 2,
contributionList: [ contributionList: [
{ {
@ -84,11 +84,11 @@ const defaultData = () => {
describe('Overview', () => { describe('Overview', () => {
let wrapper let wrapper
const adminListAllContributionsMock = jest.fn() const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
adminListAllContributions, adminListContributions,
adminListAllContributionsMock adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -109,8 +109,8 @@ describe('Overview', () => {
}) })
}) })
it('calls the adminListAllContributions query', () => { it('calls the adminListContributions query', () => {
expect(adminListAllContributionsMock).toBeCalledWith({ expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,

View File

@ -31,7 +31,7 @@
</div> </div>
</template> </template>
<script> <script>
import { adminListAllContributions } from '../graphql/adminListAllContributions' import { adminListContributions } from '../graphql/adminListContributions'
export default { export default {
name: 'overview', name: 'overview',
@ -43,7 +43,7 @@ export default {
apollo: { apollo: {
AllContributions: { AllContributions: {
query() { query() {
return adminListAllContributions return adminListContributions
}, },
variables() { variables() {
// may be at some point we need a pagination here // may be at some point we need a pagination here
@ -51,8 +51,8 @@ export default {
statusFilter: this.statusFilter, statusFilter: this.statusFilter,
} }
}, },
update({ adminListAllContributions }) { update({ adminListContributions }) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount) this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
}, },
error({ message }) { error({ message }) {
this.toastError(message) this.toastError(message)

View File

@ -1,5 +1,5 @@
# Server # Server
JWT_EXPIRES_IN=1m JWT_EXPIRES_IN=2m
# Email # Email
EMAIL=true EMAIL=true

View File

@ -0,0 +1 @@
declare module 'klicktipp-api'

View File

@ -5,6 +5,11 @@ module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 85,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'], setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'],
modulePathIgnorePatterns: ['<rootDir>/build/'], modulePathIgnorePatterns: ['<rootDir>/build/'],

View File

@ -13,7 +13,7 @@
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts", "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 .", "lint": "eslint --max-warnings=0 .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts", "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts",
"locales": "scripts/sort.sh" "locales": "scripts/sort.sh"
@ -72,6 +72,7 @@
"faker": "^5.5.3", "faker": "^5.5.3",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"jest": "^27.2.4", "jest": "^27.2.4",
"klicktipp-api": "^1.0.2",
"nodemon": "^2.0.7", "nodemon": "^2.0.7",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"ts-jest": "^27.0.5", "ts-jest": "^27.0.5",

View File

@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { KlicktippConnector } from './klicktippConnector' import KlicktippConnector from 'klicktipp-api'
import CONFIG from '@/config' import CONFIG from '@/config'
const klicktippConnector = new KlicktippConnector() const klicktippConnector = new KlicktippConnector()

View File

@ -1,624 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import axios, { AxiosRequestConfig, Method } from 'axios'
export class KlicktippConnector {
private baseURL: string
private sessionName: string
private sessionId: string
private error: string
constructor(service?: string) {
this.baseURL = service !== undefined ? service : 'https://api.klicktipp.com'
this.sessionName = ''
this.sessionId = ''
}
/**
* Get last error
*
* @return string an error description of the last error
*/
getLastError(): string {
const result = this.error
return result
}
/**
* login
*
* @param username The login name of the user to login.
* @param password The password of the user.
* @return TRUE on success
*/
async login(username: string, password: string): Promise<boolean> {
if (!(username.length > 0 && password.length > 0)) {
throw new Error('Klicktipp Login failed: Illegal Arguments')
}
const res = await this.httpRequest('/account/login', 'POST', { username, password }, false)
if (!res.isAxiosError) {
this.sessionId = res.data.sessid
this.sessionName = res.data.session_name
return true
}
throw new Error(`Klicktipp Login failed: ${res.response.statusText}`)
}
/**
* Logs out the user currently logged in.
*
* @return TRUE on success
*/
async logout(): Promise<boolean> {
const res = await this.httpRequest('/account/logout', 'POST')
if (!res.isAxiosError) {
this.sessionId = ''
this.sessionName = ''
return true
}
throw new Error(`Klicktipp Logout failed: ${res.response.statusText}`)
}
/**
* Get all subscription processes (lists) of the logged in user. Requires to be logged in.
*
* @return A associative obeject <list id> => <list name>
*/
async subscriptionProcessIndex(): Promise<any> {
const res = await this.httpRequest('/list', 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscription process index failed: ${res.response.statusText}`)
}
/**
* Get subscription process (list) definition. Requires to be logged in.
*
* @param listid The id of the subscription process
*
* @return An object representing the Klicktipp subscription process.
*/
async subscriptionProcessGet(listid: string): Promise<any> {
if (!listid || listid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// retrieve
const res = await this.httpRequest(`/subscriber/${listid}`, 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscription process get failed: ${res.response.statusText}`)
}
/**
* Get subscription process (list) redirection url for given subscription.
*
* @param listid The id of the subscription process.
* @param email The email address of the subscriber.
*
* @return A redirection url as defined in the subscription process.
*/
async subscriptionProcessRedirect(listid: string, email: string): Promise<any> {
if (!listid || listid === '' || !email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// update
const data = { listid, email }
const res = await this.httpRequest('/list/redirect', 'POST', data)
if (!res.isAxiosError) {
return res.data
}
throw new Error(
`Klicktipp Subscription process get redirection url failed: ${res.response.statusText}`,
)
}
/**
* Get all manual tags of the logged in user. Requires to be logged in.
*
* @return A associative object <tag id> => <tag name>
*/
async tagIndex(): Promise<any> {
const res = await this.httpRequest('/tag', 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tag index failed: ${res.response.statusText}`)
}
/**
* Get a tag definition. Requires to be logged in.
*
* @param tagid The tag id.
*
* @return An object representing the Klicktipp tag object.
*/
async tagGet(tagid: string): Promise<any> {
if (!tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
const res = await this.httpRequest(`/tag/${tagid}`, 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tag get failed: ${res.response.statusText}`)
}
/**
* Create a new manual tag. Requires to be logged in.
*
* @param name The name of the tag.
* @param text (optional) An additional description of the tag.
*
* @return The id of the newly created tag or false if failed.
*/
async tagCreate(name: string, text?: string): Promise<boolean> {
if (!name || name === '') {
throw new Error('Klicktipp Illegal Arguments')
}
const data = {
name,
text: text !== undefined ? text : '',
}
const res = await this.httpRequest('/tag', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tag creation failed: ${res.response.statusText}`)
}
/**
* Updates a tag. Requires to be logged in.
*
* @param tagid The tag id used to identify which tag to modify.
* @param name (optional) The new tag name. Set empty to leave it unchanged.
* @param text (optional) The new tag description. Set empty to leave it unchanged.
*
* @return TRUE on success
*/
async tagUpdate(tagid: string, name?: string, text?: string): Promise<boolean> {
if (!tagid || tagid === '' || (name === '' && text === '')) {
throw new Error('Klicktipp Illegal Arguments')
}
const data = {
name: name !== undefined ? name : '',
text: text !== undefined ? text : '',
}
const res = await this.httpRequest(`/tag/${tagid}`, 'PUT', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Tag update failed: ${res.response.statusText}`)
}
/**
* Deletes a tag. Requires to be logged in.
*
* @param tagid The user id of the user to delete.
*
* @return TRUE on success
*/
async tagDelete(tagid: string): Promise<boolean> {
if (!tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
const res = await this.httpRequest(`/tag/${tagid}`, 'DELETE')
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Tag deletion failed: ${res.response.statusText}`)
}
/**
* Get all contact fields of the logged in user. Requires to be logged in.
*
* @return A associative object <field id> => <field name>
*/
async fieldIndex(): Promise<any> {
const res = await this.httpRequest('/field', 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Field index failed: ${res.response.statusText}`)
}
/**
* Subscribe an email. Requires to be logged in.
*
* @param email The email address of the subscriber.
* @param listid (optional) The id subscription process.
* @param tagid (optional) The id of the manual tag the subscriber will be tagged with.
* @param fields (optional) Additional fields of the subscriber.
*
* @return An object representing the Klicktipp subscriber object.
*/
async subscribe(
email: string,
listid?: number,
tagid?: number,
fields?: any,
smsnumber?: string,
): Promise<any> {
if ((!email || email === '') && smsnumber === '') {
throw new Error('Illegal Arguments')
}
// subscribe
const data = {
email,
fields: fields !== undefined ? fields : {},
smsnumber: smsnumber !== undefined ? smsnumber : '',
listid: listid !== undefined ? listid : 0,
tagid: tagid !== undefined ? tagid : 0,
}
const res = await this.httpRequest('/subscriber', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscription failed: ${res.response.statusText}`)
}
/**
* Unsubscribe an email. Requires to be logged in.
*
* @param email The email address of the subscriber.
*
* @return TRUE on success
*/
async unsubscribe(email: string): Promise<boolean> {
if (!email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// unsubscribe
const data = { email }
const res = await this.httpRequest('/subscriber/unsubscribe', 'POST', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Unsubscription failed: ${res.response.statusText}`)
}
/**
* Tag an email. Requires to be logged in.
*
* @param email The email address of the subscriber.
* @param tagids an array of the manual tag(s) the subscriber will be tagged with.
*
* @return TRUE on success
*/
async tag(email: string, tagids: string): Promise<boolean> {
if (!email || email === '' || !tagids || tagids === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// tag
const data = {
email,
tagids,
}
const res = await this.httpRequest('/subscriber/tag', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tagging failed: ${res.response.statusText}`)
}
/**
* Untag an email. Requires to be logged in.
*
* @param mixed $email The email address of the subscriber.
* @param mixed $tagid The id of the manual tag that will be removed from the subscriber.
*
* @return TRUE on success.
*/
async untag(email: string, tagid: string): Promise<boolean> {
if (!email || email === '' || !tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// subscribe
const data = {
email,
tagid,
}
const res = await this.httpRequest('/subscriber/untag', 'POST', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Untagging failed: ${res.response.statusText}`)
}
/**
* Resend an autoresponder for an email address. Requires to be logged in.
*
* @param email A valid email address
* @param autoresponder An id of the autoresponder
*
* @return TRUE on success
*/
async resend(email: string, autoresponder: string): Promise<boolean> {
if (!email || email === '' || !autoresponder || autoresponder === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// resend/reset autoresponder
const data = { email, autoresponder }
const res = await this.httpRequest('/subscriber/resend', 'POST', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Resend failed: ${res.response.statusText}`)
}
/**
* Get all active subscribers. Requires to be logged in.
*
* @return An array of subscriber ids.
*/
async subscriberIndex(): Promise<[string]> {
const res = await this.httpRequest('/subscriber', 'GET', undefined, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscriber index failed: ${res.response.statusText}`)
}
/**
* Get subscriber information. Requires to be logged in.
*
* @param subscriberid The subscriber id.
*
* @return An object representing the Klicktipp subscriber.
*/
async subscriberGet(subscriberid: string): Promise<any> {
if (!subscriberid || subscriberid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// retrieve
const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscriber get failed: ${res.response.statusText}`)
}
/**
* Get a subscriber id by email. Requires to be logged in.
*
* @param email The email address of the subscriber.
*
* @return The id of the subscriber. Use subscriber_get to get subscriber details.
*/
async subscriberSearch(email: string): Promise<any> {
if (!email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// search
const data = { email }
const res = await this.httpRequest('/subscriber/search', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscriber search failed: ${res.response.statusText}`)
}
/**
* Get all active subscribers tagged with the given tag id. Requires to be logged in.
*
* @param tagid The id of the tag.
*
* @return An array with id -> subscription date of the tagged subscribers. Use subscriber_get to get subscriber details.
*/
async subscriberTagged(tagid: string): Promise<any> {
if (!tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// search
const data = { tagid }
const res = await this.httpRequest('/subscriber/tagged', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp subscriber tagged failed: ${res.response.statusText}`)
}
/**
* Updates a subscriber. Requires to be logged in.
*
* @param subscriberid The id of the subscriber to update.
* @param fields (optional) The fields of the subscriber to update
* @param newemail (optional) The new email of the subscriber to update
*
* @return TRUE on success
*/
async subscriberUpdate(
subscriberid: string,
fields?: any,
newemail?: string,
newsmsnumber?: string,
): Promise<boolean> {
if (!subscriberid || subscriberid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// update
const data = {
fields: fields !== undefined ? fields : {},
newemail: newemail !== undefined ? newemail : '',
newsmsnumber: newsmsnumber !== undefined ? newsmsnumber : '',
}
const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'PUT', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Subscriber update failed: ${res.response.statusText}`)
}
/**
* Delete a subscribe. Requires to be logged in.
*
* @param subscriberid The id of the subscriber to update.
*
* @return TRUE on success.
*/
async subscriberDelete(subscriberid: string): Promise<boolean> {
if (!subscriberid || subscriberid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// delete
const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'DELETE', {}, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Subscriber deletion failed: ${res.response.statusText}`)
}
/**
* Subscribe an email. Requires an api key.
*
* @param apikey The api key (listbuildng configuration).
* @param email The email address of the subscriber.
* @param fields (optional) Additional fields of the subscriber.
*
* @return A redirection url as defined in the subscription process.
*/
async signin(apikey: string, email: string, fields?: any, smsnumber?: string): Promise<boolean> {
if (!apikey || apikey === '' || ((!email || email === '') && smsnumber === '')) {
throw new Error('Klicktipp Illegal Arguments')
}
// subscribe
const data = {
apikey,
email,
fields: fields !== undefined ? fields : {},
smsnumber: smsnumber !== undefined ? smsnumber : '',
}
const res = await this.httpRequest('/subscriber/signin', 'POST', data)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Subscription failed: ${res.response.statusText}`)
}
/**
* Untag an email. Requires an api key.
*
* @param apikey The api key (listbuildng configuration).
* @param email The email address of the subscriber.
*
* @return TRUE on success
*/
async signout(apikey: string, email: string): Promise<boolean> {
if (!apikey || apikey === '' || !email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// untag
const data = { apikey, email }
const res = await this.httpRequest('/subscriber/signout', 'POST', data)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Untagging failed: ${res.response.statusText}`)
}
/**
* Unsubscribe an email. Requires an api key.
*
* @param apikey The api key (listbuildng configuration).
* @param email The email address of the subscriber.
*
* @return TRUE on success
*/
async signoff(apikey: string, email: string): Promise<boolean> {
if (!apikey || apikey === '' || !email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// unsubscribe
const data = { apikey, email }
const res = await this.httpRequest('/subscriber/signoff', 'POST', data)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Unsubscription failed: ${res.response.statusText}`)
}
async httpRequest(path: string, method?: Method, data?: any, usesession?: boolean): Promise<any> {
if (method === undefined) {
method = 'GET'
}
const options: AxiosRequestConfig = {
baseURL: this.baseURL,
method,
url: path,
data,
headers: {
'Content-Type': 'application/json',
Content: 'application/json',
Cookie:
usesession && this.sessionName !== '' ? `${this.sessionName}=${this.sessionId}` : '',
},
}
return axios(options)
.then((res) => res)
.catch((error) => error)
}
}

View File

@ -2,7 +2,6 @@ import { RIGHTS } from './RIGHTS'
export const INALIENABLE_RIGHTS = [ export const INALIENABLE_RIGHTS = [
RIGHTS.LOGIN, RIGHTS.LOGIN,
RIGHTS.GET_COMMUNITY_INFO,
RIGHTS.COMMUNITIES, RIGHTS.COMMUNITIES,
RIGHTS.CREATE_USER, RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL, RIGHTS.SEND_RESET_PASSWORD_EMAIL,

View File

@ -2,7 +2,6 @@ export enum RIGHTS {
LOGIN = 'LOGIN', LOGIN = 'LOGIN',
VERIFY_LOGIN = 'VERIFY_LOGIN', VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE', BALANCE = 'BALANCE',
GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO',
COMMUNITIES = 'COMMUNITIES', COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES', LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID', EXIST_PID = 'EXIST_PID',
@ -44,14 +43,14 @@ export enum RIGHTS {
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION', ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION', ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS', ADMIN_LIST_CONTRIBUTIONS = 'ADMIN_LIST_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION', CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL', SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN', LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION', DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
} }

View File

@ -11,7 +11,7 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0060-update_communities_table', DB_VERSION: '0063-event_link_fields',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -0,0 +1,6 @@
import { User as DbUser } from '@entity/User'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ACTIVATE_ACCOUNT = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.ACTIVATE_ACCOUNT, user, user).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_CONFIRM = async (
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_CONFIRM,
user,
moderator,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_CREATE,
user,
moderator,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_DELETE,
user,
moderator,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_DENY,
user,
moderator,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,23 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_LINK_CREATE = async (
moderator: DbUser,
contributionLink: DbContributionLink,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_LINK_CREATE,
{ id: 0 } as DbUser,
moderator,
null,
null,
null,
null,
null,
contributionLink,
amount,
).save()

View File

@ -0,0 +1,20 @@
import { User as DbUser } from '@entity/User'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_LINK_DELETE = async (
moderator: DbUser,
contributionLink: DbContributionLink,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_LINK_DELETE,
{ id: 0 } as DbUser,
moderator,
null,
null,
null,
null,
null,
contributionLink,
).save()

View File

@ -0,0 +1,23 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE = async (
moderator: DbUser,
contributionLink: DbContributionLink,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_LINK_UPDATE,
{ id: 0 } as DbUser,
moderator,
null,
null,
null,
null,
null,
contributionLink,
amount,
).save()

View File

@ -0,0 +1,23 @@
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE = async (
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
contributionMessage: DbContributionMessage,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_MESSAGE_CREATE,
user,
moderator,
null,
null,
contribution,
contributionMessage,
null,
null,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.ADMIN_CONTRIBUTION_UPDATE,
user,
moderator,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,8 @@
import { User as DbUser } from '@entity/User'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (
user: DbUser,
moderator: DbUser,
): Promise<DbEvent> => Event(EventType.ADMIN_SEND_CONFIRMATION_EMAIL, user, moderator).save()

View File

@ -0,0 +1,23 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_CONTRIBUTION_CREATE = async (
user: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.CONTRIBUTION_CREATE,
user,
user,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,23 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_CONTRIBUTION_DELETE = async (
user: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.CONTRIBUTION_DELETE,
user,
user,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,27 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_CONTRIBUTION_LINK_REDEEM = async (
user: DbUser,
transaction: DbTransaction,
contribution: DbContribution,
contributionLink: DbContributionLink,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.CONTRIBUTION_LINK_REDEEM,
user,
user,
null,
transaction,
contribution,
null,
null,
contributionLink,
amount,
).save()

View File

@ -0,0 +1,22 @@
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_CONTRIBUTION_MESSAGE_CREATE = async (
user: DbUser,
contribution: DbContribution,
contributionMessage: DbContributionMessage,
): Promise<DbEvent> =>
Event(
EventType.CONTRIBUTION_MESSAGE_CREATE,
user,
user,
null,
null,
contribution,
contributionMessage,
null,
null,
).save()

View File

@ -0,0 +1,23 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_CONTRIBUTION_UPDATE = async (
user: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.CONTRIBUTION_UPDATE,
user,
user,
null,
null,
contribution,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,6 @@
import { User as DbUser } from '@entity/User'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_LOGIN = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.LOGIN, user, user).save()

View File

@ -0,0 +1,6 @@
import { User as DbUser } from '@entity/User'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_REGISTER = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.REGISTER, user, user).save()

View File

@ -0,0 +1,6 @@
import { User as DbUser } from '@entity/User'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, user, { id: 0 } as DbUser).save()

View File

@ -0,0 +1,6 @@
import { User as DbUser } from '@entity/User'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_SEND_CONFIRMATION_EMAIL = async (user: DbUser): Promise<DbEvent> =>
Event(EventType.SEND_CONFIRMATION_EMAIL, user, user).save()

View File

@ -0,0 +1,23 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_TRANSACTION_LINK_CREATE = async (
user: DbUser,
transactionLink: DbTransactionLink,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.TRANSACTION_LINK_CREATE,
user,
user,
null,
null,
null,
null,
transactionLink,
null,
amount,
).save()

View File

@ -0,0 +1,19 @@
import { User as DbUser } from '@entity/User'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_TRANSACTION_LINK_DELETE = async (
user: DbUser,
transactionLink: DbTransactionLink,
): Promise<DbEvent> =>
Event(
EventType.TRANSACTION_LINK_DELETE,
user,
user,
null,
null,
null,
null,
transactionLink,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_TRANSACTION_LINK_REDEEM = async (
user: DbUser,
senderUser: DbUser,
transactionLink: DbTransactionLink,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.TRANSACTION_LINK_REDEEM,
user,
user,
senderUser,
null,
null,
null,
transactionLink,
null,
amount,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_TRANSACTION_RECEIVE = async (
user: DbUser,
involvedUser: DbUser,
transaction: DbTransaction,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.TRANSACTION_RECEIVE,
user,
involvedUser,
involvedUser,
transaction,
null,
null,
null,
null,
amount,
).save()

View File

@ -0,0 +1,24 @@
import Decimal from 'decimal.js-light'
import { User as DbUser } from '@entity/User'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Event as DbEvent } from '@entity/Event'
import { Event, EventType } from './Event'
export const EVENT_TRANSACTION_SEND = async (
user: DbUser,
involvedUser: DbUser,
transaction: DbTransaction,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventType.TRANSACTION_SEND,
user,
user,
involvedUser,
transaction,
null,
null,
null,
null,
amount,
).save()

View File

@ -1,212 +1,63 @@
import { EventProtocol as DbEvent } from '@entity/EventProtocol' import { Event as DbEvent } from '@entity/Event'
import { Decimal } from 'decimal.js-light' import { User as DbUser } from '@entity/User'
import { EventProtocolType } from './EventProtocolType' import { Transaction as DbTransaction } from '@entity/Transaction'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import Decimal from 'decimal.js-light'
import { EventType } from './Event'
export const Event = ( export const Event = (
type: EventProtocolType, type: EventType,
userId: number, affectedUser: DbUser,
xUserId: number | null = null, actingUser: DbUser,
xCommunityId: number | null = null, involvedUser: DbUser | null = null,
transactionId: number | null = null, involvedTransaction: DbTransaction | null = null,
contributionId: number | null = null, involvedContribution: DbContribution | null = null,
involvedContributionMessage: DbContributionMessage | null = null,
involvedTransactionLink: DbTransactionLink | null = null,
involvedContributionLink: DbContributionLink | null = null,
amount: Decimal | null = null, amount: Decimal | null = null,
messageId: number | null = null,
): DbEvent => { ): DbEvent => {
const event = new DbEvent() const event = new DbEvent()
event.type = type event.type = type
event.userId = userId event.affectedUser = affectedUser
event.xUserId = xUserId event.actingUser = actingUser
event.xCommunityId = xCommunityId event.involvedUser = involvedUser
event.transactionId = transactionId event.involvedTransaction = involvedTransaction
event.contributionId = contributionId event.involvedContribution = involvedContribution
event.involvedContributionMessage = involvedContributionMessage
event.involvedTransactionLink = involvedTransactionLink
event.involvedContributionLink = involvedContributionLink
event.amount = amount event.amount = amount
event.messageId = messageId
return event return event
} }
export const EVENT_CONTRIBUTION_CREATE = async ( export { EventType } from './EventType'
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_DELETE = async ( export { EVENT_ACTIVATE_ACCOUNT } from './EVENT_ACTIVATE_ACCOUNT'
userId: number, export { EVENT_ADMIN_CONTRIBUTION_CONFIRM } from './EVENT_ADMIN_CONTRIBUTION_CONFIRM'
contributionId: number, export { EVENT_ADMIN_CONTRIBUTION_CREATE } from './EVENT_ADMIN_CONTRIBUTION_CREATE'
amount: Decimal, export { EVENT_ADMIN_CONTRIBUTION_DELETE } from './EVENT_ADMIN_CONTRIBUTION_DELETE'
): Promise<DbEvent> => export { EVENT_ADMIN_CONTRIBUTION_DENY } from './EVENT_ADMIN_CONTRIBUTION_DENY'
Event( export { EVENT_ADMIN_CONTRIBUTION_UPDATE } from './EVENT_ADMIN_CONTRIBUTION_UPDATE'
EventProtocolType.CONTRIBUTION_DELETE, export { EVENT_ADMIN_CONTRIBUTION_LINK_CREATE } from './EVENT_ADMIN_CONTRIBUTION_LINK_CREATE'
userId, export { EVENT_ADMIN_CONTRIBUTION_LINK_DELETE } from './EVENT_ADMIN_CONTRIBUTION_LINK_DELETE'
null, export { EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE } from './EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE'
null, export { EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE } from './EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE'
null, export { EVENT_ADMIN_SEND_CONFIRMATION_EMAIL } from './EVENT_ADMIN_SEND_CONFIRMATION_EMAIL'
contributionId, export { EVENT_CONTRIBUTION_CREATE } from './EVENT_CONTRIBUTION_CREATE'
amount, export { EVENT_CONTRIBUTION_DELETE } from './EVENT_CONTRIBUTION_DELETE'
).save() export { EVENT_CONTRIBUTION_UPDATE } from './EVENT_CONTRIBUTION_UPDATE'
export { EVENT_CONTRIBUTION_MESSAGE_CREATE } from './EVENT_CONTRIBUTION_MESSAGE_CREATE'
export const EVENT_CONTRIBUTION_UPDATE = async ( export { EVENT_CONTRIBUTION_LINK_REDEEM } from './EVENT_CONTRIBUTION_LINK_REDEEM'
userId: number, export { EVENT_LOGIN } from './EVENT_LOGIN'
contributionId: number, export { EVENT_REGISTER } from './EVENT_REGISTER'
amount: Decimal, export { EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL } from './EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL'
): Promise<DbEvent> => export { EVENT_SEND_CONFIRMATION_EMAIL } from './EVENT_SEND_CONFIRMATION_EMAIL'
Event( export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND'
EventProtocolType.CONTRIBUTION_UPDATE, export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE'
userId, export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE'
null, export { EVENT_TRANSACTION_LINK_DELETE } from './EVENT_TRANSACTION_LINK_DELETE'
null, export { EVENT_TRANSACTION_LINK_REDEEM } from './EVENT_TRANSACTION_LINK_REDEEM'
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_CONFIRM = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CONFIRM,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
userId: number,
xUserId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
userId,
xUserId,
null,
null,
contributionId,
amount,
).save()
export const EVENT_TRANSACTION_SEND = async (
userId: number,
xUserId: number,
transactionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.TRANSACTION_SEND,
userId,
xUserId,
null,
transactionId,
null,
amount,
).save()
export const EVENT_TRANSACTION_RECEIVE = async (
userId: number,
xUserId: number,
transactionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.TRANSACTION_RECEIVE,
userId,
xUserId,
null,
transactionId,
null,
amount,
).save()
export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
userId: number,
): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
/* export const EVENT_REDEEM_REGISTER = async (
userId: number,
transactionId: number | null = null,
contributionId: number | null = null,
): Promise<Event> =>
Event(
EventProtocolType.REDEEM_REGISTER,
userId,
null,
null,
transactionId,
contributionId,
).save()
*/
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.REGISTER, userId).save()
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()

View File

@ -1,50 +1,53 @@
export enum EventProtocolType { export enum EventType {
// VISIT_GRADIDO = 'VISIT_GRADIDO',
REGISTER = 'REGISTER',
REDEEM_REGISTER = 'REDEEM_REGISTER',
// VERIFY_REDEEM = 'VERIFY_REDEEM',
// INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
// CONFIRM_EMAIL = 'CONFIRM_EMAIL',
// REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
LOGIN = 'LOGIN',
// LOGOUT = 'LOGOUT',
// REDEEM_LOGIN = 'REDEEM_LOGIN',
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT', ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
// SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL', // TODO CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
// PASSWORD_CHANGE = 'PASSWORD_CHANGE', ADMIN_CONTRIBUTION_CONFIRM = 'ADMIN_CONTRIBUTION_CONFIRM',
// SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
// SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
TRANSACTION_SEND = 'TRANSACTION_SEND',
// TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
// TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
// TRANSACTION_CREATION = 'TRANSACTION_CREATION',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
// TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
// SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
// SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
// SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
// CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
// CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
// CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY', ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_LINK_CREATE = 'ADMIN_CONTRIBUTION_LINK_CREATE',
ADMIN_CONTRIBUTION_LINK_DELETE = 'ADMIN_CONTRIBUTION_LINK_DELETE',
ADMIN_CONTRIBUTION_LINK_UPDATE = 'ADMIN_CONTRIBUTION_LINK_UPDATE',
ADMIN_CONTRIBUTION_MESSAGE_CREATE = 'ADMIN_CONTRIBUTION_MESSAGE_CREATE',
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
CONTRIBUTION_MESSAGE_CREATE = 'CONTRIBUTION_MESSAGE_CREATE',
CONTRIBUTION_LINK_REDEEM = 'CONTRIBUTION_LINK_REDEEM',
LOGIN = 'LOGIN',
REGISTER = 'REGISTER',
REDEEM_REGISTER = 'REDEEM_REGISTER',
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE',
TRANSACTION_LINK_DELETE = 'TRANSACTION_LINK_DELETE',
TRANSACTION_LINK_REDEEM = 'TRANSACTION_LINK_REDEEM',
// VISIT_GRADIDO = 'VISIT_GRADIDO',
// VERIFY_REDEEM = 'VERIFY_REDEEM',
// INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
// CONFIRM_EMAIL = 'CONFIRM_EMAIL',
// REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
// LOGOUT = 'LOGOUT',
// REDEEM_LOGIN = 'REDEEM_LOGIN',
// SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
// PASSWORD_CHANGE = 'PASSWORD_CHANGE',
// SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
// SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
// TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
// TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
// TRANSACTION_CREATION = 'TRANSACTION_CREATION',
// TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
// SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
// SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
// SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
// CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
// USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', // USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
// ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', // ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
// DELETE_USER = 'DELETE_USER', // DELETE_USER = 'DELETE_USER',
// UNDELETE_USER = 'UNDELETE_USER', // UNDELETE_USER = 'UNDELETE_USER',
// CHANGE_USER_ROLE = 'CHANGE_USER_ROLE', // CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
// ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
// ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
// CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
// DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
// UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
} }

View File

@ -21,9 +21,13 @@ export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string |
} }
} }
` `
const variables = {}
try { try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(query) const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status) logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) { if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)

View File

@ -21,9 +21,13 @@ export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string |
} }
} }
` `
const variables = {}
try { try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(query) const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status) logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) { if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey) logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)

View File

@ -1,8 +1,14 @@
import { GraphQLClient } from 'graphql-request' import { GraphQLClient } from 'graphql-request'
import { PatchedRequestInit } from 'graphql-request/dist/types' import { PatchedRequestInit } from 'graphql-request/dist/types'
type ClientInstance = {
url: string
// eslint-disable-next-line no-use-before-define
client: GraphQLGetClient
}
export class GraphQLGetClient extends GraphQLClient { export class GraphQLGetClient extends GraphQLClient {
private static instance: GraphQLGetClient private static instanceArray: ClientInstance[] = []
/** /**
* The Singleton's constructor should always be private to prevent direct * The Singleton's constructor should always be private to prevent direct
@ -20,16 +26,18 @@ export class GraphQLGetClient extends GraphQLClient {
* just one instance of each subclass around. * just one instance of each subclass around.
*/ */
public static getInstance(url: string): GraphQLGetClient { public static getInstance(url: string): GraphQLGetClient {
if (!GraphQLGetClient.instance) { const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url)
GraphQLGetClient.instance = new GraphQLGetClient(url, { if (instance) {
return instance.client
}
const client = new GraphQLGetClient(url, {
method: 'GET', method: 'GET',
jsonSerializer: { jsonSerializer: {
parse: JSON.parse, parse: JSON.parse,
stringify: JSON.stringify, stringify: JSON.stringify,
}, },
}) })
} GraphQLGetClient.instanceArray.push({ url, client } as ClientInstance)
return client
return GraphQLGetClient.instance
} }
} }

View File

@ -42,17 +42,18 @@ export async function validateCommunities(): Promise<void> {
pubKey, pubKey,
`${dbCom.endPoint}/${dbCom.apiVersion}`, `${dbCom.endPoint}/${dbCom.apiVersion}`,
) )
if (pubKey && pubKey === dbCom.publicKey.toString('hex')) { if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`) logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() }) await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`) logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`)
} else {
logger.warn(
`Federation: received not matching publicKey -> received: ${
pubKey || 'null'
}, expected: ${dbCom.publicKey.toString()} `,
)
// DbCommunity.delete({ id: dbCom.id })
} }
/*
else {
logger.warn(`Federation: received unknown publicKey -> delete dbCom with id=${dbCom.id} `)
DbCommunity.delete({ id: dbCom.id })
}
*/
} catch (err) { } catch (err) {
if (!isLogError(err)) { if (!isLogError(err)) {
logger.error(`Error:`, err) logger.error(`Error:`, err)

View File

@ -1,33 +1,47 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Int } from 'type-graphql' import { ObjectType, Field, Int } from 'type-graphql'
import { Community as DbCommunity } from '@entity/Community'
@ObjectType() @ObjectType()
export class Community { export class Community {
constructor(json?: any) { constructor(dbCom: DbCommunity) {
if (json) { this.id = dbCom.id
this.id = Number(json.id) this.foreign = dbCom.foreign
this.name = json.name this.publicKey = dbCom.publicKey.toString()
this.url = json.url this.url =
this.description = json.description (dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') +
this.registerUrl = json.registerUrl 'api/' +
} dbCom.apiVersion
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
this.verifiedAt = dbCom.verifiedAt
this.lastErrorAt = dbCom.lastErrorAt
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
} }
@Field(() => Int) @Field(() => Int)
id: number id: number
@Field(() => Boolean)
foreign: boolean
@Field(() => String) @Field(() => String)
name: string publicKey: string
@Field(() => String) @Field(() => String)
url: string url: string
@Field(() => String) @Field(() => Date, { nullable: true })
description: string lastAnnouncedAt: Date | null
@Field(() => String) @Field(() => Date, { nullable: true })
registerUrl: string verifiedAt: Date | null
@Field(() => Date, { nullable: true })
lastErrorAt: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
} }

View File

@ -1,24 +1,25 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing' import { getCommunities } from '@/seeds/graphql/queries'
import createServer from '@/server/createServer' import { Community as DbCommunity } from '@entity/Community'
import CONFIG from '@/config' import { testEnvironment } from '@test/helpers'
jest.mock('@/config')
let query: any let query: any
// to do: We need a setup for the tests that closes the connection // to do: We need a setup for the tests that closes the connection
let con: any let con: any
let testEnv: any
beforeAll(async () => { beforeAll(async () => {
const server = await createServer({}) testEnv = await testEnvironment()
con = server.con query = testEnv.query
query = createTestClient(server.apollo).query con = testEnv.con
await DbCommunity.clear()
}) })
afterAll(async () => { afterAll(async () => {
@ -26,74 +27,90 @@ afterAll(async () => {
}) })
describe('CommunityResolver', () => { describe('CommunityResolver', () => {
const getCommunityInfoQuery = ` describe('getCommunities', () => {
query { let homeCom1: DbCommunity
getCommunityInfo { let homeCom2: DbCommunity
name let homeCom3: DbCommunity
description let foreignCom1: DbCommunity
url let foreignCom2: DbCommunity
registerUrl let foreignCom3: DbCommunity
} describe('with empty list', () => {
} it('returns no community entry', async () => {
` // const result: Community[] = await query({ query: getCommunities })
// expect(result.length).toEqual(0)
const communities = ` await expect(query({ query: getCommunities })).resolves.toMatchObject({
query {
communities {
id
name
url
description
registerUrl
}
}
`
describe('getCommunityInfo', () => {
it('returns the default values', async () => {
await expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
data: { data: {
getCommunityInfo: { getCommunities: [],
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register',
},
}, },
}) })
}) })
}) })
describe('communities', () => { describe('only home-communities entries', () => {
describe('PRODUCTION = false', () => { beforeEach(async () => {
beforeEach(() => { jest.clearAllMocks()
CONFIG.PRODUCTION = false
homeCom1 = DbCommunity.create()
homeCom1.foreign = false
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.apiVersion = '1_0'
homeCom1.endPoint = 'http://localhost'
homeCom1.createdAt = new Date()
await DbCommunity.insert(homeCom1)
homeCom2 = DbCommunity.create()
homeCom2.foreign = false
homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom2.apiVersion = '1_1'
homeCom2.endPoint = 'http://localhost'
homeCom2.createdAt = new Date()
await DbCommunity.insert(homeCom2)
homeCom3 = DbCommunity.create()
homeCom3.foreign = false
homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom3.apiVersion = '2_0'
homeCom3.endPoint = 'http://localhost'
homeCom3.createdAt = new Date()
await DbCommunity.insert(homeCom3)
}) })
it('returns three communities', async () => { it('returns three home-community entries', async () => {
await expect(query({ query: communities })).resolves.toMatchObject({ await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: { data: {
communities: [ getCommunities: [
{ {
id: 1, id: 1,
name: 'Gradido Entwicklung', foreign: homeCom1.foreign,
description: 'Die lokale Entwicklungsumgebung von Gradido.', publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: 'http://localhost/', url: expect.stringMatching('http://localhost/api/1_0'),
registerUrl: 'http://localhost/register-community', lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom1.createdAt.toISOString(),
updatedAt: null,
}, },
{ {
id: 2, id: 2,
name: 'Gradido Staging', foreign: homeCom2.foreign,
description: 'Der Testserver der Gradido-Akademie.', publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: 'https://stage1.gradido.net/', url: expect.stringMatching('http://localhost/api/1_1'),
registerUrl: 'https://stage1.gradido.net/register-community', lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom2.createdAt.toISOString(),
updatedAt: null,
}, },
{ {
id: 3, id: 3,
name: 'Gradido-Akademie', foreign: homeCom3.foreign,
description: 'Freies Institut für Wirtschaftsbionik.', publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: 'https://gradido.net', url: expect.stringMatching('http://localhost/api/2_0'),
registerUrl: 'https://gdd1.gradido.com/register-community', lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
}, },
], ],
}, },
@ -101,21 +118,104 @@ describe('CommunityResolver', () => {
}) })
}) })
describe('PRODUCTION = true', () => { describe('plus foreign-communities entries', () => {
beforeEach(() => { beforeEach(async () => {
CONFIG.PRODUCTION = true jest.clearAllMocks()
foreignCom1 = DbCommunity.create()
foreignCom1.foreign = true
foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom1.apiVersion = '1_0'
foreignCom1.endPoint = 'http://remotehost'
foreignCom1.createdAt = new Date()
await DbCommunity.insert(foreignCom1)
foreignCom2 = DbCommunity.create()
foreignCom2.foreign = true
foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom2.apiVersion = '1_1'
foreignCom2.endPoint = 'http://remotehost'
foreignCom2.createdAt = new Date()
await DbCommunity.insert(foreignCom2)
foreignCom3 = DbCommunity.create()
foreignCom3.foreign = true
foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom3.apiVersion = '1_2'
foreignCom3.endPoint = 'http://remotehost'
foreignCom3.createdAt = new Date()
await DbCommunity.insert(foreignCom3)
}) })
it('returns one community', async () => { it('returns 3x home and 3x foreign-community entries', async () => {
await expect(query({ query: communities })).resolves.toMatchObject({ await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: { data: {
communities: [ getCommunities: [
{
id: 1,
foreign: homeCom1.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 2,
foreign: homeCom2.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom2.createdAt.toISOString(),
updatedAt: null,
},
{ {
id: 3, id: 3,
name: 'Gradido-Akademie', foreign: homeCom3.foreign,
description: 'Freies Institut für Wirtschaftsbionik.', publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: 'https://gradido.net', url: expect.stringMatching('http://localhost/api/2_0'),
registerUrl: 'https://gdd1.gradido.com/register-community', lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
},
{
id: 4,
foreign: foreignCom1.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 5,
foreign: foreignCom2.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 6,
foreign: foreignCom3.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_2'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom3.createdAt.toISOString(),
updatedAt: null,
}, },
], ],
}, },

View File

@ -1,58 +1,18 @@
import { Resolver, Query, Authorized } from 'type-graphql' import { Resolver, Query, Authorized } from 'type-graphql'
import { Community } from '@model/Community' import { Community } from '@model/Community'
import { Community as DbCommunity } from '@entity/Community'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import CONFIG from '@/config'
@Resolver() @Resolver()
export class CommunityResolver { export class CommunityResolver {
@Authorized([RIGHTS.GET_COMMUNITY_INFO])
@Query(() => Community)
getCommunityInfo(): Community {
return new Community({
name: CONFIG.COMMUNITY_NAME,
description: CONFIG.COMMUNITY_DESCRIPTION,
url: CONFIG.COMMUNITY_URL,
registerUrl: CONFIG.COMMUNITY_REGISTER_URL,
})
}
@Authorized([RIGHTS.COMMUNITIES]) @Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community]) @Query(() => [Community])
communities(): Community[] { async getCommunities(): Promise<Community[]> {
if (CONFIG.PRODUCTION) const dbCommunities: DbCommunity[] = await DbCommunity.find({
return [ order: { foreign: 'ASC', publicKey: 'ASC', apiVersion: 'ASC' },
new Community({ })
id: 3, return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
}),
]
return [
new Community({
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register-community',
}),
new Community({
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/',
registerUrl: 'https://stage1.gradido.net/register-community',
}),
new Community({
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
}),
]
} }
} }

View File

@ -19,6 +19,8 @@ import { cleanDB, testEnvironment, resetToken } from '@test/helpers'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { EventType } from '@/event/Event'
import { Event as DbEvent } from '@entity/Event'
let mutate: any, query: any, con: any let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
@ -249,6 +251,18 @@ describe('Contribution Links', () => {
) )
}) })
it('stores the ADMIN_CONTRIBUTION_LINK_CREATE event in the database', async () => {
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_CONTRIBUTION_LINK_CREATE,
affectedUserId: 0,
actingUserId: expect.any(Number),
involvedContributionLinkId: expect.any(Number),
amount: expect.decimalEqual(200),
}),
)
})
it('returns an error if missing startDate', async () => { it('returns an error if missing startDate', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(
@ -531,6 +545,18 @@ describe('Contribution Links', () => {
}), }),
) )
}) })
it('stores the ADMIN_CONTRIBUTION_LINK_UPDATE event in the database', async () => {
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_CONTRIBUTION_LINK_UPDATE,
affectedUserId: 0,
actingUserId: expect.any(Number),
involvedContributionLinkId: expect.any(Number),
amount: expect.decimalEqual(400),
}),
)
})
}) })
}) })
@ -558,18 +584,29 @@ describe('Contribution Links', () => {
linkId = links.data.listContributionLinks.links[0].id linkId = links.data.listContributionLinks.links[0].id
}) })
it('returns a date string', async () => { it('returns true', async () => {
await expect( await expect(
mutate({ mutation: deleteContributionLink, variables: { id: linkId } }), mutate({ mutation: deleteContributionLink, variables: { id: linkId } }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { data: {
deleteContributionLink: expect.any(String), deleteContributionLink: true,
}, },
}), }),
) )
}) })
it('stores the ADMIN_CONTRIBUTION_LINK_DELETE event in the database', async () => {
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_CONTRIBUTION_LINK_DELETE,
affectedUserId: 0,
actingUserId: expect.any(Number),
involvedContributionLinkId: linkId,
}),
)
})
it('does not list this contribution link anymore', async () => { it('does not list this contribution link anymore', async () => {
await expect(query({ query: listContributionLinks })).resolves.toEqual( await expect(query({ query: listContributionLinks })).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({

View File

@ -1,5 +1,5 @@
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { Resolver, Args, Arg, Authorized, Mutation, Query, Int } from 'type-graphql' import { Resolver, Args, Arg, Authorized, Mutation, Query, Int, Ctx } from 'type-graphql'
import { MoreThan, IsNull } from '@dbTools/typeorm' import { MoreThan, IsNull } from '@dbTools/typeorm'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
@ -14,13 +14,18 @@ import { transactionLinkCode as contributionLinkCode } from './TransactionLinkRe
import { ContributionLinkList } from '@model/ContributionLinkList' import { ContributionLinkList } from '@model/ContributionLinkList'
import { ContributionLink } from '@model/ContributionLink' import { ContributionLink } from '@model/ContributionLink'
import ContributionLinkArgs from '@arg/ContributionLinkArgs' import ContributionLinkArgs from '@arg/ContributionLinkArgs'
import { backendLogger as logger } from '@/server/logger'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
import Paginated from '@arg/Paginated' import Paginated from '@arg/Paginated'
// TODO: this is a strange construct // TODO: this is a strange construct
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { Context, getUser } from '@/server/context'
import {
EVENT_ADMIN_CONTRIBUTION_LINK_CREATE,
EVENT_ADMIN_CONTRIBUTION_LINK_DELETE,
EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE,
} from '@/event/Event'
@Resolver() @Resolver()
export class ContributionLinkResolver { export class ContributionLinkResolver {
@ -38,6 +43,7 @@ export class ContributionLinkResolver {
maxAmountPerMonth = null, maxAmountPerMonth = null,
maxPerCycle, maxPerCycle,
}: ContributionLinkArgs, }: ContributionLinkArgs,
@Ctx() context: Context,
): Promise<ContributionLink> { ): Promise<ContributionLink> {
isStartEndDateValid(validFrom, validTo) isStartEndDateValid(validFrom, validTo)
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) { if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
@ -68,7 +74,8 @@ export class ContributionLinkResolver {
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
dbContributionLink.maxPerCycle = maxPerCycle dbContributionLink.maxPerCycle = maxPerCycle
await dbContributionLink.save() await dbContributionLink.save()
logger.debug(`createContributionLink successful!`) await EVENT_ADMIN_CONTRIBUTION_LINK_CREATE(getUser(context), dbContributionLink, amount)
return new ContributionLink(dbContributionLink) return new ContributionLink(dbContributionLink)
} }
@ -91,16 +98,19 @@ export class ContributionLinkResolver {
} }
@Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK]) @Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK])
@Mutation(() => Date, { nullable: true }) @Mutation(() => Boolean)
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> { async deleteContributionLink(
const contributionLink = await DbContributionLink.findOne(id) @Arg('id', () => Int) id: number,
if (!contributionLink) { @Ctx() context: Context,
): Promise<boolean> {
const dbContributionLink = await DbContributionLink.findOne(id)
if (!dbContributionLink) {
throw new LogError('Contribution Link not found', id) throw new LogError('Contribution Link not found', id)
} }
await contributionLink.softRemove() await dbContributionLink.softRemove()
logger.debug(`deleteContributionLink successful!`) await EVENT_ADMIN_CONTRIBUTION_LINK_DELETE(getUser(context), dbContributionLink)
const newContributionLink = await DbContributionLink.findOne({ id }, { withDeleted: true })
return newContributionLink ? newContributionLink.deletedAt : null return true
} }
@Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK]) @Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK])
@ -118,6 +128,7 @@ export class ContributionLinkResolver {
maxPerCycle, maxPerCycle,
}: ContributionLinkArgs, }: ContributionLinkArgs,
@Arg('id', () => Int) id: number, @Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise<ContributionLink> { ): Promise<ContributionLink> {
const dbContributionLink = await DbContributionLink.findOne(id) const dbContributionLink = await DbContributionLink.findOne(id)
if (!dbContributionLink) { if (!dbContributionLink) {
@ -132,7 +143,8 @@ export class ContributionLinkResolver {
dbContributionLink.maxAmountPerMonth = maxAmountPerMonth dbContributionLink.maxAmountPerMonth = maxAmountPerMonth
dbContributionLink.maxPerCycle = maxPerCycle dbContributionLink.maxPerCycle = maxPerCycle
await dbContributionLink.save() await dbContributionLink.save()
logger.debug(`updateContributionLink successful!`) await EVENT_ADMIN_CONTRIBUTION_LINK_UPDATE(getUser(context), dbContributionLink, amount)
return new ContributionLink(dbContributionLink) return new ContributionLink(dbContributionLink)
} }
} }

View File

@ -20,6 +20,8 @@ import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
import { EventType } from '@/event/Event'
import { Event as DbEvent } from '@entity/Event'
jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants') const originalModule = jest.requireActual('@/emails/sendEmailVariants')
@ -197,6 +199,18 @@ describe('ContributionMessageResolver', () => {
contributionMemo: 'Test env contribution', contributionMemo: 'Test env contribution',
}) })
}) })
it('stores the ADMIN_CONTRIBUTION_MESSAGE_CREATE event in the database', async () => {
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_CONTRIBUTION_MESSAGE_CREATE,
affectedUserId: expect.any(Number),
actingUserId: expect.any(Number),
involvedContributionId: result.data.createContribution.id,
involvedContributionMessageId: expect.any(Number),
}),
)
})
}) })
}) })
}) })
@ -322,6 +336,18 @@ describe('ContributionMessageResolver', () => {
}), }),
) )
}) })
it('stores the CONTRIBUTION_MESSAGE_CREATE event in the database', async () => {
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.CONTRIBUTION_MESSAGE_CREATE,
affectedUserId: expect.any(Number),
actingUserId: expect.any(Number),
involvedContributionId: result.data.createContribution.id,
involvedContributionMessageId: expect.any(Number),
}),
)
})
}) })
}) })
}) })

View File

@ -4,7 +4,8 @@ import { getConnection } from '@dbTools/typeorm'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage' import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { Contribution as DbContribution } from '@entity/Contribution' import { Contribution as DbContribution } from '@entity/Contribution'
import { UserContact } from '@entity/UserContact' import { UserContact as DbUserContact } from '@entity/UserContact'
import { User as DbUser } from '@entity/User'
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage' import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
import ContributionMessageArgs from '@arg/ContributionMessageArgs' import ContributionMessageArgs from '@arg/ContributionMessageArgs'
@ -17,6 +18,10 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context' import { Context, getUser } from '@/server/context'
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import {
EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE,
EVENT_CONTRIBUTION_MESSAGE_CREATE,
} from '@/event/Event'
@Resolver() @Resolver()
export class ContributionMessageResolver { export class ContributionMessageResolver {
@ -57,6 +62,11 @@ export class ContributionMessageResolver {
await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution) await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
} }
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
await EVENT_CONTRIBUTION_MESSAGE_CREATE(
user,
{ id: contributionMessage.contributionId } as DbContribution,
contributionMessage,
)
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
@ -98,7 +108,7 @@ export class ContributionMessageResolver {
@Args() { contributionId, message }: ContributionMessageArgs, @Args() { contributionId, message }: ContributionMessageArgs,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<ContributionMessage> { ): Promise<ContributionMessage> {
const user = getUser(context) const moderator = getUser(context)
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
@ -112,18 +122,18 @@ export class ContributionMessageResolver {
if (!contribution) { if (!contribution) {
throw new LogError('Contribution not found', contributionId) throw new LogError('Contribution not found', contributionId)
} }
if (contribution.userId === user.id) { if (contribution.userId === moderator.id) {
throw new LogError('Admin can not answer on his own contribution', contributionId) throw new LogError('Admin can not answer on his own contribution', contributionId)
} }
if (!contribution.user.emailContact) { if (!contribution.user.emailContact) {
contribution.user.emailContact = await UserContact.findOneOrFail({ contribution.user.emailContact = await DbUserContact.findOneOrFail({
where: { id: contribution.user.emailId }, where: { id: contribution.user.emailId },
}) })
} }
contributionMessage.contributionId = contributionId contributionMessage.contributionId = contributionId
contributionMessage.createdAt = new Date() contributionMessage.createdAt = new Date()
contributionMessage.message = message contributionMessage.message = message
contributionMessage.userId = user.id contributionMessage.userId = moderator.id
contributionMessage.type = ContributionMessageType.DIALOG contributionMessage.type = ContributionMessageType.DIALOG
contributionMessage.isModerator = true contributionMessage.isModerator = true
await queryRunner.manager.insert(DbContributionMessage, contributionMessage) await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
@ -142,17 +152,23 @@ export class ContributionMessageResolver {
lastName: contribution.user.lastName, lastName: contribution.user.lastName,
email: contribution.user.emailContact.email, email: contribution.user.emailContact.email,
language: contribution.user.language, language: contribution.user.language,
senderFirstName: user.firstName, senderFirstName: moderator.firstName,
senderLastName: user.lastName, senderLastName: moderator.lastName,
contributionMemo: contribution.memo, contributionMemo: contribution.memo,
}) })
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
await EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE(
{ id: contribution.userId } as DbUser,
moderator,
contribution,
contributionMessage,
)
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e) throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
return new ContributionMessage(contributionMessage, user) return new ContributionMessage(contributionMessage, moderator)
} }
} }

View File

@ -8,7 +8,6 @@
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { EventProtocol } from '@entity/EventProtocol'
import { Contribution } from '@entity/Contribution' import { Contribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction' import { Transaction as DbTransaction } from '@entity/Transaction'
import { User } from '@entity/User' import { User } from '@entity/User'
@ -33,7 +32,7 @@ import {
import { import {
listAllContributions, listAllContributions,
listContributions, listContributions,
adminListAllContributions, adminListContributions,
} from '@/seeds/graphql/queries' } from '@/seeds/graphql/queries'
import { import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
@ -51,7 +50,11 @@ import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation' import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index' import { creations } from '@/seeds/creation/index'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { EventProtocolType } from '@/event/EventProtocolType' import { Event as DbEvent } from '@entity/Event'
import { Contribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { EventType } from '@/event/Event'
import { logger, i18n as localization } from '@test/testSetup' import { logger, i18n as localization } from '@test/testSetup'
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
@ -279,12 +282,13 @@ describe('ContributionResolver', () => {
}) })
it('stores the CONTRIBUTION_CREATE event in the database', async () => { it('stores the CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CREATE, type: EventType.CONTRIBUTION_CREATE,
affectedUserId: bibi.id,
actingUserId: bibi.id,
involvedContributionId: pendingContribution.data.createContribution.id,
amount: expect.decimalEqual(100), amount: expect.decimalEqual(100),
contributionId: pendingContribution.data.createContribution.id,
userId: bibi.id,
}), }),
) )
}) })
@ -584,12 +588,13 @@ describe('ContributionResolver', () => {
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_UPDATE, type: EventType.CONTRIBUTION_UPDATE,
affectedUserId: bibi.id,
actingUserId: bibi.id,
involvedContributionId: pendingContribution.data.createContribution.id,
amount: expect.decimalEqual(10), amount: expect.decimalEqual(10),
contributionId: pendingContribution.data.createContribution.id,
userId: bibi.id,
}), }),
) )
}) })
@ -814,12 +819,12 @@ describe('ContributionResolver', () => {
}) })
it('stores the ADMIN_CONTRIBUTION_DENY event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_DENY event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_DENY, type: EventType.ADMIN_CONTRIBUTION_DENY,
userId: bibi.id, affectedUserId: bibi.id,
xUserId: admin.id, actingUserId: admin.id,
contributionId: contributionToDeny.data.createContribution.id, involvedContributionId: contributionToDeny.data.createContribution.id,
amount: expect.decimalEqual(100), amount: expect.decimalEqual(100),
}), }),
) )
@ -942,12 +947,13 @@ describe('ContributionResolver', () => {
}) })
it('stores the CONTRIBUTION_DELETE event in the database', async () => { it('stores the CONTRIBUTION_DELETE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_DELETE, type: EventType.CONTRIBUTION_DELETE,
contributionId: contributionToDelete.data.createContribution.id, affectedUserId: bibi.id,
actingUserId: bibi.id,
involvedContributionId: contributionToDelete.data.createContribution.id,
amount: expect.decimalEqual(100), amount: expect.decimalEqual(100),
userId: bibi.id,
}), }),
) )
}) })
@ -2031,10 +2037,11 @@ describe('ContributionResolver', () => {
}) })
it('stores the ADMIN_CONTRIBUTION_CREATE event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE, type: EventType.ADMIN_CONTRIBUTION_CREATE,
userId: admin.id, affectedUserId: bibi.id,
actingUserId: admin.id,
amount: expect.decimalEqual(200), amount: expect.decimalEqual(200),
}), }),
) )
@ -2233,7 +2240,7 @@ describe('ContributionResolver', () => {
mutate({ mutate({
mutation: adminUpdateContribution, mutation: adminUpdateContribution,
variables: { variables: {
id: creation ? creation.id : -1, id: creation?.id,
email: 'peter@lustig.de', email: 'peter@lustig.de',
amount: new Decimal(300), amount: new Decimal(300),
memo: 'Danke Peter!', memo: 'Danke Peter!',
@ -2257,10 +2264,11 @@ describe('ContributionResolver', () => {
}) })
it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, type: EventType.ADMIN_CONTRIBUTION_UPDATE,
userId: admin.id, affectedUserId: creation?.userId,
actingUserId: admin.id,
amount: 300, amount: 300,
}), }),
) )
@ -2274,7 +2282,7 @@ describe('ContributionResolver', () => {
mutate({ mutate({
mutation: adminUpdateContribution, mutation: adminUpdateContribution,
variables: { variables: {
id: creation ? creation.id : -1, id: creation?.id,
email: 'peter@lustig.de', email: 'peter@lustig.de',
amount: new Decimal(200), amount: new Decimal(200),
memo: 'Das war leider zu Viel!', memo: 'Das war leider zu Viel!',
@ -2298,10 +2306,11 @@ describe('ContributionResolver', () => {
}) })
it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, type: EventType.ADMIN_CONTRIBUTION_UPDATE,
userId: admin.id, affectedUserId: creation?.userId,
actingUserId: admin.id,
amount: expect.decimalEqual(200), amount: expect.decimalEqual(200),
}), }),
) )
@ -2372,7 +2381,7 @@ describe('ContributionResolver', () => {
mutate({ mutate({
mutation: adminDeleteContribution, mutation: adminDeleteContribution,
variables: { variables: {
id: creation ? creation.id : -1, id: creation?.id,
}, },
}), }),
).resolves.toEqual( ).resolves.toEqual(
@ -2383,10 +2392,12 @@ describe('ContributionResolver', () => {
}) })
it('stores the ADMIN_CONTRIBUTION_DELETE event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_DELETE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, type: EventType.ADMIN_CONTRIBUTION_DELETE,
userId: admin.id, affectedUserId: creation?.userId,
actingUserId: admin.id,
involvedContributionId: creation?.id,
amount: expect.decimalEqual(200), amount: expect.decimalEqual(200),
}), }),
) )
@ -2538,10 +2549,10 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the CONTRIBUTION_CONFIRM event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_CONFIRM event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CONFIRM, type: EventType.ADMIN_CONTRIBUTION_CONFIRM,
}), }),
) )
}) })
@ -2571,9 +2582,9 @@ describe('ContributionResolver', () => {
}) })
it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => { it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventType.SEND_CONFIRMATION_EMAIL,
}), }),
) )
}) })
@ -2662,12 +2673,12 @@ describe('ContributionResolver', () => {
}) })
}) })
describe('adminListAllContribution', () => { describe('adminListContributions', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {
await expect( await expect(
query({ query({
query: adminListAllContributions, query: adminListContributions,
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
@ -2692,7 +2703,7 @@ describe('ContributionResolver', () => {
it('returns an error', async () => { it('returns an error', async () => {
await expect( await expect(
query({ query({
query: adminListAllContributions, query: adminListContributions,
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
@ -2716,9 +2727,9 @@ describe('ContributionResolver', () => {
it('returns 17 creations in total', async () => { it('returns 17 creations in total', async () => {
const { const {
data: { adminListAllContributions: contributionListObject }, data: { adminListContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({ }: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListAllContributions, query: adminListContributions,
}) })
expect(contributionListObject.contributionList).toHaveLength(17) expect(contributionListObject.contributionList).toHaveLength(17)
expect(contributionListObject).toMatchObject({ expect(contributionListObject).toMatchObject({
@ -2883,9 +2894,9 @@ describe('ContributionResolver', () => {
it('returns two pending creations with page size set to 2', async () => { it('returns two pending creations with page size set to 2', async () => {
const { const {
data: { adminListAllContributions: contributionListObject }, data: { adminListContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({ }: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListAllContributions, query: adminListContributions,
variables: { variables: {
currentPage: 1, currentPage: 1,
pageSize: 2, pageSize: 2,

View File

@ -9,13 +9,6 @@ import { UserContact } from '@entity/UserContact'
import { User as DbUser } from '@entity/User' import { User as DbUser } from '@entity/User'
import { Transaction as DbTransaction } from '@entity/Transaction' import { Transaction as DbTransaction } from '@entity/Transaction'
import {
getCreationDates,
getUserCreation,
validateContribution,
updateCreations,
isValidDateString,
} from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
import { findContributions } from './util/findContributions' import { findContributions } from './util/findContributions'
@ -37,6 +30,13 @@ import AdminUpdateContributionArgs from '@arg/AdminUpdateContributionArgs'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import {
getUserCreation,
validateContribution,
updateCreations,
isValidDateString,
getOpenCreations,
} from './util/creations'
import { import {
EVENT_CONTRIBUTION_CREATE, EVENT_CONTRIBUTION_CREATE,
EVENT_CONTRIBUTION_DELETE, EVENT_CONTRIBUTION_DELETE,
@ -44,7 +44,7 @@ import {
EVENT_ADMIN_CONTRIBUTION_CREATE, EVENT_ADMIN_CONTRIBUTION_CREATE,
EVENT_ADMIN_CONTRIBUTION_UPDATE, EVENT_ADMIN_CONTRIBUTION_UPDATE,
EVENT_ADMIN_CONTRIBUTION_DELETE, EVENT_ADMIN_CONTRIBUTION_DELETE,
EVENT_CONTRIBUTION_CONFIRM, EVENT_ADMIN_CONTRIBUTION_CONFIRM,
EVENT_ADMIN_CONTRIBUTION_DENY, EVENT_ADMIN_CONTRIBUTION_DENY,
} from '@/event/Event' } from '@/event/Event'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
@ -89,8 +89,7 @@ export class ContributionResolver {
logger.trace('contribution to save', contribution) logger.trace('contribution to save', contribution)
await DbContribution.save(contribution) await DbContribution.save(contribution)
await EVENT_CONTRIBUTION_CREATE(user, contribution, amount)
await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
return new UnconfirmedContribution(contribution, user, creations) return new UnconfirmedContribution(contribution, user, creations)
} }
@ -117,8 +116,7 @@ export class ContributionResolver {
contribution.deletedBy = user.id contribution.deletedBy = user.id
contribution.deletedAt = new Date() contribution.deletedAt = new Date()
await contribution.save() await contribution.save()
await EVENT_CONTRIBUTION_DELETE(user, contribution, contribution.amount)
await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
const res = await contribution.softRemove() const res = await contribution.softRemove()
return !!res return !!res
@ -135,15 +133,15 @@ export class ContributionResolver {
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
const user = getUser(context) const user = getUser(context)
const [dbContributions, count] = await findContributions( const [dbContributions, count] = await findContributions({
order, order,
currentPage, currentPage,
pageSize, pageSize,
true, withDeleted: true,
['messages'], relations: ['messages'],
user.id, userId: user.id,
statusFilter, statusFilter,
) })
return new ContributionListResult( return new ContributionListResult(
count, count,
dbContributions.map((contribution) => new Contribution(contribution, user)), dbContributions.map((contribution) => new Contribution(contribution, user)),
@ -158,15 +156,13 @@ export class ContributionResolver {
@Arg('statusFilter', () => [ContributionStatus], { nullable: true }) @Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null, statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions( const [dbContributions, count] = await findContributions({
order, order,
currentPage, currentPage,
pageSize, pageSize,
false, relations: ['user'],
['user'],
undefined,
statusFilter, statusFilter,
) })
return new ContributionListResult( return new ContributionListResult(
count, count,
@ -248,7 +244,7 @@ export class ContributionResolver {
contributionToUpdate.updatedAt = new Date() contributionToUpdate.updatedAt = new Date()
await DbContribution.save(contributionToUpdate) await DbContribution.save(contributionToUpdate)
await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount) await EVENT_CONTRIBUTION_UPDATE(user, contributionToUpdate, amount)
return new UnconfirmedContribution(contributionToUpdate, user, creations) return new UnconfirmedContribution(contributionToUpdate, user, creations)
} }
@ -300,12 +296,9 @@ export class ContributionResolver {
contribution.moderatorId = moderator.id contribution.moderatorId = moderator.id
contribution.contributionType = ContributionType.ADMIN contribution.contributionType = ContributionType.ADMIN
contribution.contributionStatus = ContributionStatus.PENDING contribution.contributionStatus = ContributionStatus.PENDING
logger.trace('contribution to save', contribution) logger.trace('contribution to save', contribution)
await DbContribution.save(contribution) await DbContribution.save(contribution)
await EVENT_ADMIN_CONTRIBUTION_CREATE(emailContact.user, moderator, contribution, amount)
await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
return getUserCreation(emailContact.userId, clientTimezoneOffset) return getUserCreation(emailContact.userId, clientTimezoneOffset)
} }
@ -370,31 +363,36 @@ export class ContributionResolver {
result.amount = amount result.amount = amount
result.memo = contributionToUpdate.memo result.memo = contributionToUpdate.memo
result.date = contributionToUpdate.contributionDate result.date = contributionToUpdate.contributionDate
result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset) result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
await EVENT_ADMIN_CONTRIBUTION_UPDATE(
await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount) emailContact.user,
moderator,
contributionToUpdate,
amount,
)
return result return result
} }
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS]) @Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult) // [UnconfirmedContribution] @Query(() => ContributionListResult)
async adminListAllContributions( async adminListContributions(
@Args() @Args()
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated, { currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true }) @Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null, statusFilter?: ContributionStatus[] | null,
@Arg('userId', () => Int, { nullable: true })
userId?: number | null,
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions( const [dbContributions, count] = await findContributions({
order, order,
currentPage, currentPage,
pageSize, pageSize,
true, withDeleted: true,
['user', 'messages'], userId,
undefined, relations: ['user', 'messages'],
statusFilter, statusFilter,
) })
return new ContributionListResult( return new ContributionListResult(
count, count,
@ -430,8 +428,12 @@ export class ContributionResolver {
contribution.deletedBy = moderator.id contribution.deletedBy = moderator.id
await contribution.save() await contribution.save()
const res = await contribution.softRemove() const res = await contribution.softRemove()
await EVENT_ADMIN_CONTRIBUTION_DELETE(
await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount) { id: contribution.userId } as DbUser,
moderator,
contribution,
contribution.amount,
)
void sendContributionDeletedEmail({ void sendContributionDeletedEmail({
firstName: user.firstName, firstName: user.firstName,
@ -543,58 +545,26 @@ export class ContributionResolver {
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
await EVENT_ADMIN_CONTRIBUTION_CONFIRM(user, moderatorUser, contribution, contribution.amount)
await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
} finally { } finally {
releaseLock() releaseLock()
} }
return true return true
} }
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
@Query(() => ContributionListResult)
async creationTransactionList(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Arg('userId', () => Int) userId: number,
): Promise<ContributionListResult> {
const offset = (currentPage - 1) * pageSize
const [contributionResult, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(DbContribution, 'c')
.leftJoinAndSelect('c.user', 'u')
.where(`user_id = ${userId}`)
.withDeleted()
.limit(pageSize)
.offset(offset)
.orderBy('c.created_at', order)
.getManyAndCount()
return new ContributionListResult(
count,
contributionResult.map((contribution) => new Contribution(contribution, contribution.user)),
)
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
}
@Authorized([RIGHTS.OPEN_CREATIONS]) @Authorized([RIGHTS.OPEN_CREATIONS])
@Query(() => [OpenCreation]) @Query(() => [OpenCreation])
async openCreations( async openCreations(@Ctx() context: Context): Promise<OpenCreation[]> {
@Ctx() context: Context, return getOpenCreations(getUser(context).id, getClientTimezoneOffset(context))
@Arg('userId', () => Int, { nullable: true }) userId?: number | null,
): Promise<OpenCreation[]> {
const id = userId || getUser(context).id
const clientTimezoneOffset = getClientTimezoneOffset(context)
const creationDates = getCreationDates(clientTimezoneOffset)
const creations = await getUserCreation(id, clientTimezoneOffset)
return creationDates.map((date, index) => {
return {
month: date.getMonth(),
year: date.getFullYear(),
amount: creations[index],
} }
})
@Authorized([RIGHTS.ADMIN_OPEN_CREATIONS])
@Query(() => [OpenCreation])
async adminOpenCreations(
@Arg('userId', () => Int) userId: number,
@Ctx() context: Context,
): Promise<OpenCreation[]> {
return getOpenCreations(userId, getClientTimezoneOffset(context))
} }
@Authorized([RIGHTS.DENY_CONTRIBUTION]) @Authorized([RIGHTS.DENY_CONTRIBUTION])
@ -633,11 +603,10 @@ export class ContributionResolver {
contributionToUpdate.deniedBy = moderator.id contributionToUpdate.deniedBy = moderator.id
contributionToUpdate.deniedAt = new Date() contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save() const res = await contributionToUpdate.save()
await EVENT_ADMIN_CONTRIBUTION_DENY( await EVENT_ADMIN_CONTRIBUTION_DENY(
contributionToUpdate.userId, user,
moderator.id, moderator,
contributionToUpdate.id, contributionToUpdate,
contributionToUpdate.amount, contributionToUpdate.amount,
) )

View File

@ -26,11 +26,18 @@ import {
createContribution, createContribution,
updateContribution, updateContribution,
createTransactionLink, createTransactionLink,
confirmContribution,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { User } from '@entity/User'
import { Transaction } from '@entity/Transaction'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution' import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
import { EventType } from '@/event/Event'
import { Event as DbEvent } from '@entity/Event'
import { UserContact } from '@entity/UserContact'
// mock semaphore to allow use fake timers // mock semaphore to allow use fake timers
jest.mock('@/util/TRANSACTIONS_LOCK') jest.mock('@/util/TRANSACTIONS_LOCK')
@ -142,6 +149,8 @@ describe('TransactionLinkResolver', () => {
resetToken() resetToken()
}) })
let contributionId: number
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -311,7 +320,6 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
// TODO: have this test separated into a transactionLink and a contributionLink part
describe('redeem daily Contribution Link', () => { describe('redeem daily Contribution Link', () => {
const now = new Date() const now = new Date()
let contributionLink: DbContributionLink | undefined let contributionLink: DbContributionLink | undefined
@ -337,6 +345,10 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
afterAll(async () => {
await resetEntity(Transaction)
})
it('has a daily contribution link in the database', async () => { it('has a daily contribution link in the database', async () => {
const cls = await DbContributionLink.find() const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1) expect(cls).toHaveLength(1)
@ -378,6 +390,7 @@ describe('TransactionLinkResolver', () => {
}, },
}) })
contribution = result.data.createContribution contribution = result.data.createContribution
contributionId = result.data.createContribution.id
}) })
it('does not allow the user to redeem the contribution link', async () => { it('does not allow the user to redeem the contribution link', async () => {
@ -437,6 +450,24 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
it('stores the CONTRIBUTION_LINK_REDEEM event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.CONTRIBUTION_LINK_REDEEM,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
involvedTransactionId: expect.any(Number),
involvedContributionId: expect.any(Number),
involvedContributionLinkId: contributionLink?.id,
amount: contributionLink?.amount,
}),
)
})
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(
@ -513,6 +544,92 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
}) })
describe('transaction link', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('link does not exits', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('throws and logs the error', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'not-valid',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Transaction link not found')],
})
expect(logger.error).toBeCalledWith('Transaction link not found', 'not-valid')
})
})
describe('link exists', () => {
let myCode: string
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: confirmContribution,
variables: { id: contributionId },
})
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: {
createTransactionLink: { code },
},
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 200,
memo: 'This is a transaction link from bibi',
},
})
myCode = code
})
describe('own link', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('throws and logs an error', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Cannot redeem own transaction link')],
})
expect(logger.error).toBeCalledWith(
'Cannot redeem own transaction link',
expect.any(Number),
)
})
})
})
})
}) })
}) })

View File

@ -25,7 +25,6 @@ import { ContributionCycleType } from '@enum/ContributionCycleType'
import TransactionLinkArgs from '@arg/TransactionLinkArgs' import TransactionLinkArgs from '@arg/TransactionLinkArgs'
import Paginated from '@arg/Paginated' import Paginated from '@arg/Paginated'
import TransactionLinkFilters from '@arg/TransactionLinkFilters' import TransactionLinkFilters from '@arg/TransactionLinkFilters'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { Context, getUser, getClientTimezoneOffset } from '@/server/context' import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { calculateBalance } from '@/util/validate' import { calculateBalance } from '@/util/validate'
@ -34,6 +33,14 @@ import { calculateDecay } from '@/util/decay'
import QueryLinkResult from '@union/QueryLinkResult' import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
import transactionLinkList from './util/transactionLinkList'
import {
EVENT_CONTRIBUTION_LINK_REDEEM,
EVENT_TRANSACTION_LINK_CREATE,
EVENT_TRANSACTION_LINK_DELETE,
EVENT_TRANSACTION_LINK_REDEEM,
} from '@/event/Event'
// TODO: do not export, test it inside the resolver // TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => { export const transactionLinkCode = (date: Date): string => {
@ -88,6 +95,7 @@ export class TransactionLinkResolver {
await DbTransactionLink.save(transactionLink).catch((e) => { await DbTransactionLink.save(transactionLink).catch((e) => {
throw new LogError('Unable to save transaction link', e) throw new LogError('Unable to save transaction link', e)
}) })
await EVENT_TRANSACTION_LINK_CREATE(user, transactionLink, amount)
return new TransactionLink(transactionLink, new User(user)) return new TransactionLink(transactionLink, new User(user))
} }
@ -121,6 +129,8 @@ export class TransactionLinkResolver {
throw new LogError('Transaction link could not be deleted', e) throw new LogError('Transaction link could not be deleted', e)
}) })
await EVENT_TRANSACTION_LINK_DELETE(user, transactionLink)
return true return true
} }
@ -271,7 +281,13 @@ export class TransactionLinkResolver {
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution) await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('creation from contribution link commited successfuly.') await EVENT_CONTRIBUTION_LINK_REDEEM(
user,
transaction,
contribution,
contributionLink,
contributionLink.amount,
)
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError('Creation from contribution link was not successful', e) throw new LogError('Creation from contribution link was not successful', e)
@ -284,12 +300,20 @@ export class TransactionLinkResolver {
return true return true
} else { } else {
const now = new Date() const now = new Date()
const transactionLink = await DbTransactionLink.findOneOrFail({ code }) const transactionLink = await DbTransactionLink.findOne({ code })
const linkedUser = await DbUser.findOneOrFail( if (!transactionLink) {
throw new LogError('Transaction link not found', code)
}
const linkedUser = await DbUser.findOne(
{ id: transactionLink.userId }, { id: transactionLink.userId },
{ relations: ['emailContact'] }, { relations: ['emailContact'] },
) )
if (!linkedUser) {
throw new LogError('Linked user not found for given link', transactionLink.userId)
}
if (user.id === linkedUser.id) { if (user.id === linkedUser.id) {
throw new LogError('Cannot redeem own transaction link', user.id) throw new LogError('Cannot redeem own transaction link', user.id)
} }
@ -312,6 +336,12 @@ export class TransactionLinkResolver {
user, user,
transactionLink, transactionLink,
) )
await EVENT_TRANSACTION_LINK_REDEEM(
user,
{ id: transactionLink.userId } as DbUser,
transactionLink,
transactionLink.amount,
)
return true return true
} }

View File

@ -6,12 +6,11 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { EventProtocol } from '@entity/EventProtocol'
import { Transaction } from '@entity/Transaction' import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User' import { User } from '@entity/User'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { findUserByEmail } from './UserResolver' import { findUserByEmail } from './UserResolver'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventType } from '@/event/Event'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { import {
confirmContribution, confirmContribution,
@ -23,6 +22,7 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
import { Event as DbEvent } from '@entity/Event'
import { cleanDB, testEnvironment } from '@test/helpers' import { cleanDB, testEnvironment } from '@test/helpers'
import { logger } from '@test/testSetup' import { logger } from '@test/testSetup'
@ -341,12 +341,13 @@ describe('send coins', () => {
memo: 'unrepeatable memo', memo: 'unrepeatable memo',
}) })
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.TRANSACTION_SEND, type: EventType.TRANSACTION_SEND,
userId: user[1].id, affectedUserId: user[1].id,
transactionId: transaction[0].id, actingUserId: user[1].id,
xUserId: user[0].id, involvedUserId: user[0].id,
involvedTransactionId: transaction[0].id,
}), }),
) )
}) })
@ -358,12 +359,13 @@ describe('send coins', () => {
memo: 'unrepeatable memo', memo: 'unrepeatable memo',
}) })
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.TRANSACTION_RECEIVE, type: EventType.TRANSACTION_RECEIVE,
userId: user[0].id, affectedUserId: user[0].id,
transactionId: transaction[0].id, actingUserId: user[1].id,
xUserId: user[1].id, involvedUserId: user[1].id,
involvedTransactionId: transaction[0].id,
}), }),
) )
}) })

View File

@ -136,17 +136,12 @@ export const executeTransaction = async (
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`) logger.info(`commit Transaction successful...`)
await EVENT_TRANSACTION_SEND( await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
transactionSend.userId,
transactionSend.linkedUserId,
transactionSend.id,
transactionSend.amount.mul(-1),
)
await EVENT_TRANSACTION_RECEIVE( await EVENT_TRANSACTION_RECEIVE(
transactionReceive.userId, recipient,
transactionReceive.linkedUserId, sender,
transactionReceive.id, transactionReceive,
transactionReceive.amount, transactionReceive.amount,
) )
} catch (e) { } catch (e) {

View File

@ -9,7 +9,6 @@
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { User } from '@entity/User' import { User } from '@entity/User'
import { TransactionLink } from '@entity/TransactionLink' import { TransactionLink } from '@entity/TransactionLink'
import { EventProtocol } from '@entity/EventProtocol'
import { validate as validateUUID, version as versionUUID } from 'uuid' import { validate as validateUUID, version as versionUUID } from 'uuid'
import { UserContact } from '@entity/UserContact' import { UserContact } from '@entity/UserContact'
import { OptInType } from '@enum/OptInType' import { OptInType } from '@enum/OptInType'
@ -45,7 +44,10 @@ import {
import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
import { ContributionLink } from '@model/ContributionLink' import { ContributionLink } from '@model/ContributionLink'
import { EventProtocolType } from '@/event/EventProtocolType' import { TransactionLink } from '@entity/TransactionLink'
import { EventType } from '@/event/Event'
import { Event as DbEvent } from '@entity/Event'
import { validate as validateUUID, version as versionUUID } from 'uuid'
import { peterLustig } from '@/seeds/users/peter-lustig' import { peterLustig } from '@/seeds/users/peter-lustig'
import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { bobBaumeister } from '@/seeds/users/bob-baumeister'
import { stephenHawking } from '@/seeds/users/stephen-hawking' import { stephenHawking } from '@/seeds/users/stephen-hawking'
@ -187,10 +189,11 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' }, { email: 'peter@lustig.de' },
{ relations: ['user'] }, { relations: ['user'] },
) )
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REGISTER, type: EventType.REGISTER,
userId: userConatct.user.id, affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}), }),
) )
}) })
@ -216,10 +219,11 @@ describe('UserResolver', () => {
}) })
it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => { it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventType.SEND_CONFIRMATION_EMAIL,
userId: user[0].id, affectedUserId: user[0].id,
actingUserId: user[0].id,
}), }),
) )
}) })
@ -261,10 +265,11 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' }, { email: 'peter@lustig.de' },
{ relations: ['user'] }, { relations: ['user'] },
) )
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, type: EventType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
userId: userConatct.user.id, affectedUserId: userConatct.user.id,
actingUserId: 0,
}), }),
) )
}) })
@ -361,20 +366,22 @@ describe('UserResolver', () => {
}) })
it('stores the ACTIVATE_ACCOUNT event in the database', async () => { it('stores the ACTIVATE_ACCOUNT event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT, type: EventType.ACTIVATE_ACCOUNT,
userId: user[0].id, affectedUserId: user[0].id,
actingUserId: user[0].id,
}), }),
) )
}) })
it('stores the REDEEM_REGISTER event in the database', async () => { it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventType.REDEEM_REGISTER,
userId: result.data.createUser.id, affectedUserId: result.data.createUser.id,
contributionId: link.id, actingUserId: result.data.createUser.id,
involvedContributionLinkId: link.id,
}), }),
) )
}) })
@ -454,10 +461,12 @@ describe('UserResolver', () => {
}) })
it('stores the REDEEM_REGISTER event in the database', async () => { it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventType.REDEEM_REGISTER,
userId: newUser.data.createUser.id, affectedUserId: newUser.data.createUser.id,
actingUserId: newUser.data.createUser.id,
involvedTransactionLinkId: transactionLink.id,
}), }),
) )
}) })
@ -685,10 +694,11 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' }, { email: 'bibi@bloxberg.de' },
{ relations: ['user'] }, { relations: ['user'] },
) )
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.LOGIN, type: EventType.LOGIN,
userId: userConatct.user.id, affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}), }),
) )
}) })
@ -934,10 +944,11 @@ describe('UserResolver', () => {
}) })
it('stores the LOGIN event in the database', async () => { it('stores the LOGIN event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.LOGIN, type: EventType.LOGIN,
userId: user[0].id, affectedUserId: user[0].id,
actingUserId: user[0].id,
}), }),
) )
}) })
@ -1853,10 +1864,11 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' }, { email: 'bibi@bloxberg.de' },
{ relations: ['user'] }, { relations: ['user'] },
) )
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, type: EventType.ADMIN_SEND_CONFIRMATION_EMAIL,
userId: userConatct.user.id, affectedUserId: userConatct.user.id,
actingUserId: admin.id,
}), }),
) )
}) })

View File

@ -57,6 +57,7 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys' import { hasElopageBuys } from '@/util/hasElopageBuys'
import { import {
Event, Event,
EventType,
EVENT_LOGIN, EVENT_LOGIN,
EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
EVENT_SEND_CONFIRMATION_EMAIL, EVENT_SEND_CONFIRMATION_EMAIL,
@ -67,7 +68,6 @@ import {
import { isValidPassword } from '@/password/EncryptorUtils' import { isValidPassword } from '@/password/EncryptorUtils'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { EventProtocolType } from '@/event/EventProtocolType'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const sodium = require('sodium-native') const sodium = require('sodium-native')
@ -182,7 +182,7 @@ export class UserResolver {
value: encode(dbUser.gradidoID), value: encode(dbUser.gradidoID),
}) })
await EVENT_LOGIN(user.id) await EVENT_LOGIN(dbUser)
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user return user
} }
@ -252,7 +252,7 @@ export class UserResolver {
language: foundUser.language, // use language of the emails owner for sending language: foundUser.language, // use language of the emails owner for sending
}) })
await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser.id) await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser)
logger.info( logger.info(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`, `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
@ -270,7 +270,11 @@ export class UserResolver {
const gradidoID = await newGradidoID() const gradidoID = await newGradidoID()
const eventRegisterRedeem = Event(EventProtocolType.REDEEM_REGISTER, 0) const eventRegisterRedeem = Event(
EventType.REDEEM_REGISTER,
{ id: 0 } as DbUser,
{ id: 0 } as DbUser,
)
let dbUser = new DbUser() let dbUser = new DbUser()
dbUser.gradidoID = gradidoID dbUser.gradidoID = gradidoID
dbUser.firstName = firstName dbUser.firstName = firstName
@ -287,14 +291,14 @@ export class UserResolver {
logger.info('redeemCode found contributionLink', contributionLink) logger.info('redeemCode found contributionLink', contributionLink)
if (contributionLink) { if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id dbUser.contributionLinkId = contributionLink.id
eventRegisterRedeem.contributionId = contributionLink.id eventRegisterRedeem.involvedContributionLink = contributionLink
} }
} else { } else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode }) const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink', transactionLink) logger.info('redeemCode found transactionLink', transactionLink)
if (transactionLink) { if (transactionLink) {
dbUser.referrerId = transactionLink.userId dbUser.referrerId = transactionLink.userId
eventRegisterRedeem.transactionId = transactionLink.id eventRegisterRedeem.involvedTransactionLink = transactionLink
} }
} }
} }
@ -333,7 +337,7 @@ export class UserResolver {
}) })
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
await EVENT_SEND_CONFIRMATION_EMAIL(dbUser.id) await EVENT_SEND_CONFIRMATION_EMAIL(dbUser)
if (!emailSent) { if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`) logger.debug(`Account confirmation link: ${activationLink}`)
@ -350,10 +354,11 @@ export class UserResolver {
logger.info('createUser() successful...') logger.info('createUser() successful...')
if (redeemCode) { if (redeemCode) {
eventRegisterRedeem.userId = dbUser.id eventRegisterRedeem.affectedUser = dbUser
eventRegisterRedeem.actingUser = dbUser
await eventRegisterRedeem.save() await eventRegisterRedeem.save()
} else { } else {
await EVENT_REGISTER(dbUser.id) await EVENT_REGISTER(dbUser)
} }
return new User(dbUser) return new User(dbUser)
@ -469,7 +474,7 @@ export class UserResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('User and UserContact data written successfully...') logger.info('User and UserContact data written successfully...')
await EVENT_ACTIVATE_ACCOUNT(user.id) await EVENT_ACTIVATE_ACCOUNT(user)
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e) throw new LogError('Error on writing User and User Contact data', e)
@ -779,9 +784,13 @@ export class UserResolver {
return null return null
} }
// TODO this is an admin function - needs refactor
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@Mutation(() => Boolean) @Mutation(() => Boolean)
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> { async sendActivationEmail(
@Arg('email') email: string,
@Ctx() context: Context,
): Promise<boolean> {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
// const user = await dbUser.findOne({ id: emailContact.userId }) // const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email) const user = await findUserByEmail(email)
@ -806,7 +815,7 @@ export class UserResolver {
if (!emailSent) { if (!emailSent) {
logger.info(`Account confirmation link: ${activationLink}`) logger.info(`Account confirmation link: ${activationLink}`)
} else { } else {
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id) await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user, getUser(context))
} }
return true return true

View File

@ -189,4 +189,50 @@ describe('semaphore', () => {
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined }) await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined }) await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
}) })
describe('redeem transaction link twice', () => {
let myCode: string
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bibisLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 20,
memo: 'Bibis Link',
},
})
myCode = bibisLink.code
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
})
it('does not throw, but should', async () => {
const redeem1 = mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
})
const redeem2 = mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
})
await expect(redeem1).resolves.toMatchObject({
errors: undefined,
})
await expect(redeem2).resolves.toMatchObject({
errors: undefined,
})
})
})
}) })

View File

@ -5,6 +5,7 @@ import { Contribution } from '@entity/Contribution'
import { Decimal } from 'decimal.js-light' import { Decimal } from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '@/graphql/resolver/const/const' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '@/graphql/resolver/const/const'
import { backendLogger as logger } from '@/server/logger' import { backendLogger as logger } from '@/server/logger'
import { OpenCreation } from '@model/OpenCreation'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
interface CreationMap { interface CreationMap {
@ -102,7 +103,7 @@ const getCreationMonths = (timezoneOffset: number): number[] => {
return getCreationDates(timezoneOffset).map((date) => date.getMonth() + 1) return getCreationDates(timezoneOffset).map((date) => date.getMonth() + 1)
} }
export const getCreationDates = (timezoneOffset: number): Date[] => { const getCreationDates = (timezoneOffset: number): Date[] => {
const clientNow = new Date() const clientNow = new Date()
clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000) clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
logger.info( logger.info(
@ -154,3 +155,18 @@ export const updateCreations = (
export const isValidDateString = (dateString: string): boolean => { export const isValidDateString = (dateString: string): boolean => {
return new Date(dateString).toString() !== 'Invalid Date' return new Date(dateString).toString() !== 'Invalid Date'
} }
export const getOpenCreations = async (
userId: number,
timezoneOffset: number,
): Promise<OpenCreation[]> => {
const creations = await getUserCreation(userId, timezoneOffset)
const creationDates = getCreationDates(timezoneOffset)
return creationDates.map((date, index) => {
return {
month: date.getMonth(),
year: date.getFullYear(),
amount: creations[index],
}
})
}

View File

@ -3,21 +3,30 @@ import { In } from '@dbTools/typeorm'
import { ContributionStatus } from '@enum/ContributionStatus' import { ContributionStatus } from '@enum/ContributionStatus'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
interface FindContributionsOptions {
order: Order
currentPage: number
pageSize: number
withDeleted?: boolean
relations?: string[]
userId?: number | null
statusFilter?: ContributionStatus[] | null
}
export const findContributions = async ( export const findContributions = async (
order: Order, options: FindContributionsOptions,
currentPage: number, ): Promise<[DbContribution[], number]> => {
pageSize: number, const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter } = {
withDeleted: boolean, withDeleted: false,
relations: string[], relations: [],
userId?: number, ...options,
statusFilter?: ContributionStatus[] | null, }
): Promise<[DbContribution[], number]> => return DbContribution.findAndCount({
DbContribution.findAndCount({
where: { where: {
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }), ...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }), ...(userId && { userId }),
}, },
withDeleted: withDeleted, withDeleted,
order: { order: {
createdAt: order, createdAt: order,
id: order, id: order,
@ -26,3 +35,4 @@ export const findContributions = async (
skip: (currentPage - 1) * pageSize, skip: (currentPage - 1) * pageSize,
take: pageSize, take: pageSize,
}) })
}

View File

@ -89,6 +89,12 @@ export const createTransactionLink = gql`
} }
` `
export const deleteTransactionLink = gql`
mutation ($id: Int!) {
deleteTransactionLink(id: $id)
}
`
// from admin interface // from admin interface
export const adminCreateContribution = gql` export const adminCreateContribution = gql`

View File

@ -133,6 +133,22 @@ export const communities = gql`
} }
` `
export const getCommunities = gql`
query {
getCommunities {
id
foreign
publicKey
url
lastAnnouncedAt
verifiedAt
lastErrorAt
createdAt
updatedAt
}
}
`
export const queryTransactionLink = gql` export const queryTransactionLink = gql`
query ($code: String!) { query ($code: String!) {
queryTransactionLink(code: $code) { queryTransactionLink(code: $code) {
@ -204,18 +220,20 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
` `
// from admin interface // from admin interface
export const adminListAllContributions = gql` export const adminListContributions = gql`
query ( query (
$currentPage: Int = 1 $currentPage: Int = 1
$pageSize: Int = 25 $pageSize: Int = 25
$order: Order = DESC $order: Order = DESC
$statusFilter: [ContributionStatus!] $statusFilter: [ContributionStatus!]
$userId: Int
) { ) {
adminListAllContributions( adminListContributions(
currentPage: $currentPage currentPage: $currentPage
pageSize: $pageSize pageSize: $pageSize
order: $order order: $order
statusFilter: $statusFilter statusFilter: $statusFilter
userId: $userId
) { ) {
contributionCount contributionCount
contributionList { contributionList {

View File

@ -59,7 +59,7 @@
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"] "@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
}, },
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["src/federation/@types", "node_modules/@types"], /* List of folders to include type definitions from. */ "typeRoots": ["@types", "node_modules/@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

View File

@ -4951,6 +4951,13 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
klicktipp-api@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/klicktipp-api/-/klicktipp-api-1.0.2.tgz#a7ba728887c4d9a1c257fa30b78cbe0be92a20ab"
integrity sha512-aQQpuznC0O2W7Oq2BxKDnuLAnGmKTMfudOQ0TAEf0TLv82KH2AsCXl0nbutJ2g1i3MH+sCyGE/r/nwnUhr4QeA==
dependencies:
axios "^0.21.1"
latest-version@^5.1.0: latest-version@^5.1.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"

View File

@ -25,13 +25,13 @@ export class Community extends BaseEntity {
endPoint: string endPoint: string
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true }) @Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
lastAnnouncedAt: Date lastAnnouncedAt: Date | null
@Column({ name: 'verified_at', type: 'datetime', nullable: true }) @Column({ name: 'verified_at', type: 'datetime', nullable: true })
verifiedAt: Date verifiedAt: Date | null
@Column({ name: 'last_error_at', type: 'datetime', nullable: true }) @Column({ name: 'last_error_at', type: 'datetime', nullable: true })
lastErrorAt: Date lastErrorAt: Date | null
@CreateDateColumn({ @CreateDateColumn({
name: 'created_at', name: 'created_at',

View File

@ -0,0 +1,83 @@
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
import { User } from '../User'
import { Transaction } from '../Transaction'
import Decimal from 'decimal.js-light'
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
@Entity('events')
export class Event extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
type: string
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@Column({ name: 'affected_user_id', unsigned: true, nullable: false })
affectedUserId: number
@ManyToOne(() => User)
@JoinColumn({ name: 'affected_user_id', referencedColumnName: 'id' })
affectedUser: User
@Column({ name: 'acting_user_id', unsigned: true, nullable: false })
actingUserId: number
@ManyToOne(() => User)
@JoinColumn({ name: 'acting_user_id', referencedColumnName: 'id' })
actingUser: User
@Column({ name: 'involved_user_id', type: 'int', unsigned: true, nullable: true })
involvedUserId: number | null
@ManyToOne(() => User)
@JoinColumn({ name: 'involved_user_id', referencedColumnName: 'id' })
involvedUser: User | null
@Column({ name: 'involved_transaction_id', type: 'int', unsigned: true, nullable: true })
involvedTransactionId: number | null
@ManyToOne(() => Transaction)
@JoinColumn({ name: 'involved_transaction_id', referencedColumnName: 'id' })
involvedTransaction: Transaction | null
@Column({ name: 'involved_contribution_id', type: 'int', unsigned: true, nullable: true })
involvedContributionId: number | null
@ManyToOne(() => Contribution)
@JoinColumn({ name: 'involved_contribution_id', referencedColumnName: 'id' })
involvedContribution: Contribution | null
@Column({ name: 'involved_contribution_message_id', type: 'int', unsigned: true, nullable: true })
involvedContributionMessageId: number | null
@ManyToOne(() => ContributionMessage)
@JoinColumn({ name: 'involved_contribution_message_id', referencedColumnName: 'id' })
involvedContributionMessage: ContributionMessage | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
amount: Decimal | null
}

View File

@ -0,0 +1,99 @@
import { Contribution } from '../Contribution'
import { ContributionMessage } from '../ContributionMessage'
import { User } from '../User'
import { Transaction } from '../Transaction'
import Decimal from 'decimal.js-light'
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm'
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
import { TransactionLink } from '../TransactionLink'
import { ContributionLink } from '../ContributionLink'
@Entity('events')
export class Event extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
type: string
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@Column({ name: 'affected_user_id', unsigned: true, nullable: false })
affectedUserId: number
@ManyToOne(() => User)
@JoinColumn({ name: 'affected_user_id', referencedColumnName: 'id' })
affectedUser: User
@Column({ name: 'acting_user_id', unsigned: true, nullable: false })
actingUserId: number
@ManyToOne(() => User)
@JoinColumn({ name: 'acting_user_id', referencedColumnName: 'id' })
actingUser: User
@Column({ name: 'involved_user_id', type: 'int', unsigned: true, nullable: true })
involvedUserId: number | null
@ManyToOne(() => User)
@JoinColumn({ name: 'involved_user_id', referencedColumnName: 'id' })
involvedUser: User | null
@Column({ name: 'involved_transaction_id', type: 'int', unsigned: true, nullable: true })
involvedTransactionId: number | null
@ManyToOne(() => Transaction)
@JoinColumn({ name: 'involved_transaction_id', referencedColumnName: 'id' })
involvedTransaction: Transaction | null
@Column({ name: 'involved_contribution_id', type: 'int', unsigned: true, nullable: true })
involvedContributionId: number | null
@ManyToOne(() => Contribution)
@JoinColumn({ name: 'involved_contribution_id', referencedColumnName: 'id' })
involvedContribution: Contribution | null
@Column({ name: 'involved_contribution_message_id', type: 'int', unsigned: true, nullable: true })
involvedContributionMessageId: number | null
@ManyToOne(() => ContributionMessage)
@JoinColumn({ name: 'involved_contribution_message_id', referencedColumnName: 'id' })
involvedContributionMessage: ContributionMessage | null
@Column({ name: 'involved_transaction_link_id', type: 'int', unsigned: true, nullable: true })
involvedTransactionLinkId: number | null
@ManyToOne(() => TransactionLink)
@JoinColumn({ name: 'involved_transaction_link_id', referencedColumnName: 'id' })
involvedTransactionLink: TransactionLink | null
@Column({ name: 'involved_contribution_link_id', type: 'int', unsigned: true, nullable: true })
involvedContributionLinkId: number | null
@ManyToOne(() => ContributionLink)
@JoinColumn({ name: 'involved_contribution_link_id', referencedColumnName: 'id' })
involvedContributionLink: ContributionLink | null
@Column({
type: 'decimal',
precision: 40,
scale: 20,
nullable: true,
transformer: DecimalTransformer,
})
amount: Decimal | null
}

1
database/entity/Event.ts Normal file
View File

@ -0,0 +1 @@
export { Event } from './0063-event_link_fields/Event'

View File

@ -1 +0,0 @@
export { EventProtocol } from './0050-add_messageId_to_event_protocol/EventProtocol'

View File

@ -7,21 +7,21 @@ import { TransactionLink } from './TransactionLink'
import { User } from './User' import { User } from './User'
import { UserContact } from './UserContact' import { UserContact } from './UserContact'
import { Contribution } from './Contribution' import { Contribution } from './Contribution'
import { EventProtocol } from './EventProtocol' import { Event } from './Event'
import { ContributionMessage } from './ContributionMessage' import { ContributionMessage } from './ContributionMessage'
import { Community } from './Community' import { Community } from './Community'
export const entities = [ export const entities = [
Community,
Contribution, Contribution,
ContributionLink, ContributionLink,
ContributionMessage,
Event,
LoginElopageBuys, LoginElopageBuys,
LoginEmailOptIn, LoginEmailOptIn,
Migration, Migration,
Transaction, Transaction,
TransactionLink, TransactionLink,
User, User,
EventProtocol,
ContributionMessage,
UserContact, UserContact,
Community,
] ]

View File

@ -0,0 +1,98 @@
/* MIGRATION TO REFACTOR THE EVENT_PROTOCOL TABLE
*
* This migration refactors the `event_protocol` table.
* It renames the table to `event`, introduces new fields and renames others.
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn('RENAME TABLE `event_protocol` TO `events`;')
await queryFn('ALTER TABLE `events` RENAME COLUMN `user_id` TO `affected_user_id`;')
await queryFn(
'ALTER TABLE `events` ADD COLUMN `acting_user_id` int(10) unsigned DEFAULT NULL AFTER `affected_user_id`;',
)
await queryFn('UPDATE `events` SET `acting_user_id` = `affected_user_id`;')
await queryFn(
'ALTER TABLE `events` MODIFY COLUMN `acting_user_id` int(10) unsigned NOT NULL AFTER `affected_user_id`;',
)
await queryFn('ALTER TABLE `events` RENAME COLUMN `x_user_id` TO `involved_user_id`;')
await queryFn('ALTER TABLE `events` DROP COLUMN `x_community_id`;')
await queryFn('ALTER TABLE `events` RENAME COLUMN `transaction_id` TO `involved_transaction_id`;')
await queryFn(
'ALTER TABLE `events` RENAME COLUMN `contribution_id` TO `involved_contribution_id`;',
)
await queryFn(
'ALTER TABLE `events` MODIFY COLUMN `message_id` int(10) unsigned DEFAULT NULL AFTER `involved_contribution_id`;',
)
await queryFn(
'ALTER TABLE `events` RENAME COLUMN `message_id` TO `involved_contribution_message_id`;',
)
// Moderator id was saved in former user_id
await queryFn(
'UPDATE `events` LEFT JOIN `contributions` ON events.involved_contribution_id = contributions.id SET affected_user_id=contributions.user_id WHERE `type` = "ADMIN_CONTRIBUTION_CREATE";',
)
// inconsistent data on this type, since not all data can be reconstructed
await queryFn(
'UPDATE `events` LEFT JOIN `contributions` ON events.involved_contribution_id = contributions.id SET acting_user_id=0 WHERE `type` = "ADMIN_CONTRIBUTION_UPDATE";',
)
await queryFn(
'UPDATE `events` LEFT JOIN `contributions` ON events.involved_contribution_id = contributions.id SET acting_user_id=contributions.deleted_by WHERE `type` = "ADMIN_CONTRIBUTION_DELETE";',
)
await queryFn(
'UPDATE `events` LEFT JOIN `contributions` ON events.involved_contribution_id = contributions.id SET acting_user_id=contributions.confirmed_by WHERE `type` = "CONTRIBUTION_CONFIRM";',
)
await queryFn(
'UPDATE `events` LEFT JOIN `contributions` ON events.involved_contribution_id = contributions.id SET involved_user_id=NULL, acting_user_id=contributions.denied_by WHERE `type` = "ADMIN_CONTRIBUTION_DENY";',
)
await queryFn(
'UPDATE `events` SET acting_user_id=involved_user_id WHERE `type` = "TRANSACTION_RECEIVE";',
)
await queryFn('UPDATE `events` SET amount = amount * -1 WHERE `type` = "TRANSACTION_SEND";')
await queryFn(
'ALTER TABLE `events` MODIFY COLUMN `created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3);',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `events` MODIFY COLUMN `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP();',
)
await queryFn('UPDATE `events` SET amount = amount * -1 WHERE `type` = "TRANSACTION_SEND";')
await queryFn(
'UPDATE `events` SET involved_user_id=acting_user_id WHERE `type` = "ADMIN_CONTRIBUTION_DENY";',
)
await queryFn(
'UPDATE `events` SET affected_user_id=acting_user_id WHERE `type` = "ADMIN_CONTRIBUTION_CREATE";',
)
await queryFn(
'ALTER TABLE `events` RENAME COLUMN `involved_contribution_message_id` TO `message_id`;',
)
await queryFn(
'ALTER TABLE `events` MODIFY COLUMN `message_id` int(10) unsigned DEFAULT NULL AFTER `amount`;',
)
await queryFn(
'ALTER TABLE `events` RENAME COLUMN `involved_contribution_id` TO `contribution_id`;',
)
await queryFn('ALTER TABLE `events` RENAME COLUMN `involved_transaction_id` TO `transaction_id`;')
await queryFn(
'ALTER TABLE `events` ADD COLUMN `x_community_id` int(10) unsigned DEFAULT NULL AFTER `involved_user_id`;',
)
await queryFn('ALTER TABLE `events` RENAME COLUMN `involved_user_id` TO `x_user_id`;')
await queryFn('ALTER TABLE `events` DROP COLUMN `acting_user_id`;')
await queryFn('ALTER TABLE `events` RENAME COLUMN `affected_user_id` TO `user_id`;')
await queryFn('RENAME TABLE `events` TO `event_protocol`;')
}

View File

@ -0,0 +1,20 @@
/* MIGRATION TO RENAME CONTRIBUTION_CONFIRM EVENT
*
* This migration renames the CONTRIBUTION_CONFIRM Event
* to ADMIN_CONTRIBUTION_CONFIRM
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'UPDATE `events` SET `type` = "ADMIN_CONTRIBUTION_CONFIRM" WHERE `type` = "CONTRIBUTION_CONFIRM";',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'UPDATE `events` SET `type` = "CONTRIBUTION_CONFIRM" WHERE `type` = "ADMIN_CONTRIBUTION_CONFIRM";',
)
}

View File

@ -0,0 +1,29 @@
/* MIGRATION TO ADD LINK ID FIELDS TO EVENT TABLE
*
* This migration add two fields to store a TransactionLinkId and a ContributionLinkId
* in the event table. Furthermore the event `REDEEM_REGISTER` is rewritten to use the
* new fields.
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `events` ADD COLUMN `involved_transaction_link_id` int(10) unsigned DEFAULT NULL AFTER `involved_contribution_message_id`;',
)
await queryFn(
'ALTER TABLE `events` ADD COLUMN `involved_contribution_link_id` int(10) unsigned DEFAULT NULL AFTER `involved_transaction_link_id`;',
)
await queryFn(
'UPDATE `events` SET `involved_transaction_link_id` = `involved_transaction_id`, `involved_transaction_id` = NULL, `involved_contribution_link_id` = `involved_contribution_id`, `involved_contribution_id` = NULL WHERE `type` = "REDEEM_REGISTER";',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'UPDATE `events` SET `involved_transaction_id` = `involved_transaction_link_id`, `involved_contribution_id` = `involved_contribution_link_id` WHERE `type` = "REDEEM_REGISTER";',
)
await queryFn('ALTER TABLE `events` DROP COLUMN `involved_contribution_link_id`;')
await queryFn('ALTER TABLE `events` DROP COLUMN `involved_transaction_link_id`;')
}

View File

@ -16,9 +16,7 @@
"dev_up": "cross-env TZ=UTC ts-node src/index.ts up", "dev_up": "cross-env TZ=UTC ts-node src/index.ts up",
"dev_down": "cross-env TZ=UTC ts-node src/index.ts down", "dev_down": "cross-env TZ=UTC ts-node src/index.ts down",
"dev_reset": "cross-env TZ=UTC ts-node src/index.ts reset", "dev_reset": "cross-env TZ=UTC ts-node src/index.ts reset",
"lint": "eslint --max-warnings=0 --ext .js,.ts .", "lint": "eslint --max-warnings=0 --ext .js,.ts ."
"seed:config": "ts-node ./node_modules/typeorm-seeding/dist/cli.js config",
"seed": "cross-env TZ=UTC ts-node src/index.ts seed"
}, },
"devDependencies": { "devDependencies": {
"@types/faker": "^5.5.9", "@types/faker": "^5.5.9",

View File

@ -65,7 +65,9 @@ FEDERATION_COMMUNITY_URL=http://stage1.gradido.net
# the api port is the baseport, which will be added with the api-version, e.g. 1_0 = 5010 # the api port is the baseport, which will be added with the api-version, e.g. 1_0 = 5010
FEDERATION_COMMUNITY_API_PORT=5000 FEDERATION_COMMUNITY_API_PORT=5000
FEDERATION_CONFIG_VERSION=v1.2023-01-09
# comma separated list of api-versions, which cause starting several federation modules
FEDERATION_COMMUNITY_APIS=1_0,1_1,2_0
# database # database
DATABASE_CONFIG_VERSION=v1.2022-03-18 DATABASE_CONFIG_VERSION=v1.2022-03-18

View File

@ -0,0 +1,15 @@
location /api/$FEDERATION_APIVERSION {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:$FEDERATION_PORT;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.federation-$FEDERATION_PORT.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.federation-$FEDERATION_PORT.log warn;
}

View File

@ -112,6 +112,9 @@ server {
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
} }
# Federation
$FEDERATION_NGINX_CONF
# TODO this could be a performance optimization # TODO this could be a performance optimization
#location /vue { #location /vue {
# alias /var/www/html/gradido/frontend/dist; # alias /var/www/html/gradido/frontend/dist;

View File

@ -98,6 +98,9 @@ server {
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn; error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
} }
# Federation
$FEDERATION_NGINX_CONF
# TODO this could be a performance optimization # TODO this could be a performance optimization
#location /vue { #location /vue {
# alias /var/www/html/gradido/frontend/dist; # alias /var/www/html/gradido/frontend/dist;

View File

@ -59,8 +59,9 @@ ln -s /etc/nginx/sites-available/update-page.conf /etc/nginx/sites-enabled/
sudo /etc/init.d/nginx restart sudo /etc/init.d/nginx restart
# stop all services # stop all services
echo 'Stopping all Gradido services' >> $UPDATE_HTML echo 'Stop and delete all Gradido services' >> $UPDATE_HTML
pm2 stop all pm2 delete all
pm2 save
# git # git
BRANCH=${1:-master} BRANCH=${1:-master}
@ -73,12 +74,41 @@ git pull
export BUILD_COMMIT="$(git rev-parse HEAD)" export BUILD_COMMIT="$(git rev-parse HEAD)"
# Generate gradido.conf from template # Generate gradido.conf from template
# *** 1st prepare for each apiversion the federation conf for nginx from federation-template
# *** set FEDERATION_PORT from FEDERATION_COMMUNITY_APIS and create gradido-federation.conf file
rm -f $NGINX_CONFIG_DIR/gradido.conf.tmp
rm -f $NGINX_CONFIG_DIR/gradido-federation.conf.locations
echo "====================================================================================================" >> $UPDATE_HTML
IFS="," read -a API_ARRAY <<< $FEDERATION_COMMUNITY_APIS
for api in "${API_ARRAY[@]}"
do
export FEDERATION_APIVERSION=$api
# calculate port by remove '_' and add value of api to baseport
port=${api//_/}
FEDERATION_PORT=${FEDERATION_COMMUNITY_API_PORT:-5000}
FEDERATION_PORT=$(($FEDERATION_PORT + $port))
export FEDERATION_PORT
echo "create ngingx config: location /api/$FEDERATION_APIVERSION to http://127.0.0.1:$FEDERATION_PORT" >> $UPDATE_HTML
envsubst '$FEDERATION_APIVERSION, $FEDERATION_PORT' < $NGINX_CONFIG_DIR/gradido-federation.conf.template >> $NGINX_CONFIG_DIR/gradido-federation.conf.locations
done
unset FEDERATION_APIVERSION
unset FEDERATION_PORT
echo "====================================================================================================" >> $UPDATE_HTML
# *** 2nd read gradido-federation.conf file in env variable to be replaced in 3rd step
export FEDERATION_NGINX_CONF=$(< $NGINX_CONFIG_DIR/gradido-federation.conf.locations)
# *** 3rd generate gradido nginx config including federation modules per api-version
echo 'Generate new gradido nginx config' >> $UPDATE_HTML echo 'Generate new gradido nginx config' >> $UPDATE_HTML
case "$NGINX_SSL" in case "$NGINX_SSL" in
true) TEMPLATE_FILE="gradido.conf.ssl.template" ;; true) TEMPLATE_FILE="gradido.conf.ssl.template" ;;
*) TEMPLATE_FILE="gradido.conf.template" ;; *) TEMPLATE_FILE="gradido.conf.template" ;;
esac esac
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $NGINX_CONFIG_DIR/$TEMPLATE_FILE > $NGINX_CONFIG_DIR/gradido.conf envsubst '$FEDERATION_NGINX_CONF' < $NGINX_CONFIG_DIR/$TEMPLATE_FILE > $NGINX_CONFIG_DIR/gradido.conf.tmp
unset FEDERATION_NGINX_CONF
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $NGINX_CONFIG_DIR/gradido.conf.tmp > $NGINX_CONFIG_DIR/gradido.conf
rm $NGINX_CONFIG_DIR/gradido.conf.tmp
rm $NGINX_CONFIG_DIR/gradido-federation.conf.locations
# Generate update-page.conf from template # Generate update-page.conf from template
echo 'Generate new update-page nginx config' >> $UPDATE_HTML echo 'Generate new update-page nginx config' >> $UPDATE_HTML
@ -94,11 +124,13 @@ cp -f $PROJECT_ROOT/backend/.env $PROJECT_ROOT/backend/.env.bak
cp -f $PROJECT_ROOT/frontend/.env $PROJECT_ROOT/frontend/.env.bak cp -f $PROJECT_ROOT/frontend/.env $PROJECT_ROOT/frontend/.env.bak
cp -f $PROJECT_ROOT/admin/.env $PROJECT_ROOT/admin/.env.bak cp -f $PROJECT_ROOT/admin/.env $PROJECT_ROOT/admin/.env.bak
cp -f $PROJECT_ROOT/dht-node/.env $PROJECT_ROOT/dht-node/.env.bak cp -f $PROJECT_ROOT/dht-node/.env $PROJECT_ROOT/dht-node/.env.bak
cp -f $PROJECT_ROOT/federation/.env $PROJECT_ROOT/federation/.env.bak
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/database/.env.template > $PROJECT_ROOT/database/.env envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/database/.env.template > $PROJECT_ROOT/database/.env
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/backend/.env.template > $PROJECT_ROOT/backend/.env envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/backend/.env.template > $PROJECT_ROOT/backend/.env
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env.template > $PROJECT_ROOT/frontend/.env envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env.template > $PROJECT_ROOT/frontend/.env
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env.template > $PROJECT_ROOT/dht-node/.env envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/dht-node/.env.template > $PROJECT_ROOT/dht-node/.env
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/federation/.env.template > $PROJECT_ROOT/federation/.env
# Install & build database # Install & build database
echo 'Updating database' >> $UPDATE_HTML echo 'Updating database' >> $UPDATE_HTML
@ -124,7 +156,6 @@ if [ "$DEPLOY_SEED_DATA" = "true" ]; then
fi fi
# TODO maybe handle this differently? # TODO maybe handle this differently?
export NODE_ENV=production export NODE_ENV=production
pm2 delete gradido-backend
pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save pm2 save
@ -137,7 +168,6 @@ yarn install
yarn build yarn build
# TODO maybe handle this differently? # TODO maybe handle this differently?
export NODE_ENV=production export NODE_ENV=production
pm2 delete gradido-frontend
pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save pm2 save
@ -150,7 +180,6 @@ yarn install
yarn build yarn build
# TODO maybe handle this differently? # TODO maybe handle this differently?
export NODE_ENV=production export NODE_ENV=production
pm2 delete gradido-admin
pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save pm2 save
@ -163,15 +192,49 @@ yarn install
yarn build yarn build
# TODO maybe handle this differently? # TODO maybe handle this differently?
export NODE_ENV=production export NODE_ENV=production
pm2 delete gradido-dht-node
if [ ! -z $FEDERATION_DHT_TOPIC ]; then if [ ! -z $FEDERATION_DHT_TOPIC ]; then
pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 start --name gradido-dht-node "yarn --cwd $PROJECT_ROOT/dht-node start" -l $GRADIDO_LOG_PATH/pm2.dht-node.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save
else else
echo "=====================================================================" echo "=====================================================================" >> $UPDATE_HTML
echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..." echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..." >> $UPDATE_HTML
echo "=====================================================================" echo "=====================================================================" >> $UPDATE_HTML
fi fi
pm2 save
# Install & build federation
echo 'Updating federation' >> $UPDATE_HTML
cd $PROJECT_ROOT/federation
# TODO maybe handle this differently?
unset NODE_ENV
yarn install
yarn build
# TODO maybe handle this differently?
export NODE_ENV=production
# set FEDERATION_PORT from FEDERATION_COMMUNITY_APIS
IFS="," read -a API_ARRAY <<< $FEDERATION_COMMUNITY_APIS
for api in "${API_ARRAY[@]}"
do
export FEDERATION_API=$api
echo "FEDERATION_API=$FEDERATION_API" >> $UPDATE_HTML
export MODULENAME=gradido-federation-$api
echo "MODULENAME=$MODULENAME" >> $UPDATE_HTML
# calculate port by remove '_' and add value of api to baseport
port=${api//_/}
FEDERATION_PORT=${FEDERATION_COMMUNITY_API_PORT:-5000}
FEDERATION_PORT=$(($FEDERATION_PORT + $port))
export FEDERATION_PORT
echo "====================================================" >> $UPDATE_HTML
echo " start $MODULENAME listening on port=$FEDERATION_PORT" >> $UPDATE_HTML
echo "====================================================" >> $UPDATE_HTML
# pm2 delete $MODULENAME
pm2 start --name $MODULENAME "yarn --cwd $PROJECT_ROOT/federation start" -l $GRADIDO_LOG_PATH/pm2.$MODULENAME.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS'
pm2 save
done
# let nginx showing gradido # let nginx showing gradido
echo 'Configuring nginx to serve gradido again' >> $UPDATE_HTML echo 'Configuring nginx to serve gradido again' >> $UPDATE_HTML

View File

@ -4,6 +4,11 @@ module.exports = {
preset: 'ts-jest', preset: 'ts-jest',
collectCoverage: true, collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 80,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'], setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: [], setupFilesAfterEnv: [],
modulePathIgnorePatterns: ['<rootDir>/build/'], modulePathIgnorePatterns: ['<rootDir>/build/'],

View File

@ -13,7 +13,7 @@
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts", "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .", "lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles" "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles"
}, },
"dependencies": { "dependencies": {
"@hyperswarm/dht": "^6.4.4", "@hyperswarm/dht": "^6.4.4",

View File

@ -3,7 +3,7 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0060-update_communities_table', DB_VERSION: '0063-event_link_fields',
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',

Some files were not shown because too many files have changed in this diff Show More