Merge branch 'master' into feat-send-coins-via-gradido-ID

This commit is contained in:
Moriz Wahl 2023-03-23 13:15:47 +01:00
commit 9c84ca185d
99 changed files with 2095 additions and 1253 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
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 #############################################
##############################################################################
@ -132,139 +81,6 @@ jobs:
name: docker-mariadb-test
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 ##########################################################
##############################################################################
@ -319,68 +135,6 @@ jobs:
- name: Database | 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 ####################################################
##############################################################################
@ -415,20 +169,7 @@ jobs:
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test
run: |
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 }}
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
##########################################################################
# DATABASE MIGRATION TEST UP + RESET #####################################
@ -452,108 +193,3 @@ jobs:
run: docker-compose -f docker-compose.yml run -T database yarn up
- name: database | 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
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 #####################################################
##############################################################################
build:
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
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build `test` image
- name: Build 'test' image
run: |
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
docker save "gradido/dht-node:test" > /tmp/dht-node.tar
@ -29,30 +50,24 @@ jobs:
##############################################################################
lint:
name: Lint - DHT Node
if: needs.files-changed.outputs.dht_node == 'true'
needs: files-changed
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
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
run: docker run --rm gradido/dht-node:test yarn run lint
run: cd dht-node && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST #############################################################
##############################################################################
unit_test:
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
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -83,16 +98,4 @@ jobs:
#- name: Unit tests
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
run: |
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 }}
run: docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/dht-node:test yarn run test

View File

@ -3,11 +3,32 @@ name: Gradido Federation 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 - 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 #####################################################
##############################################################################
build:
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
steps:
- name: Checkout code
@ -29,30 +50,24 @@ jobs:
##############################################################################
lint:
name: Lint - Federation
if: needs.files-changed.outputs.federation == 'true'
needs: files-changed
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
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
run: docker run --rm gradido/federation:test yarn run lint
run: cd federation && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST #############################################################
##############################################################################
unit_test:
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
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -84,15 +99,4 @@ jobs:
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
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
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 }}
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/federation:test yarn run test

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
```
### 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

View File

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

View File

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

View File

@ -1,43 +1,75 @@
import { mount } from '@vue/test-utils'
import CreationTransactionList from './CreationTransactionList'
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
localVue.use(VueApollo)
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
creationTransactionList: {
const defaultData = () => {
return {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
amount: 5.8,
createdAt: '2022-09-21T11:09:51.000Z',
confirmedAt: null,
contributionDate: '2022-08-01T00:00:00.000Z',
memo: 'für deine Hilfe, Fräulein Rottenmeier',
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
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,
amount: '47',
createdAt: '2022-09-21T11:09:28.000Z',
confirmedAt: '2022-09-21T11:09:28.000Z',
contributionDate: '2022-08-01T00:00:00.000Z',
memo: 'für deine Hilfe, Frau Holle',
state: 'CONFIRMED',
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
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 = {
$d: jest.fn((t) => t),
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},
}
const propsData = {
@ -48,55 +80,53 @@ const propsData = {
describe('CreationTransactionList', () => {
let wrapper
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => {
return mount(CreationTransactionList, { localVue, mocks, propsData })
return mount(CreationTransactionList, { localVue, mocks, propsData, apolloProvider })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
currentPage: 1,
pageSize: 10,
order: 'DESC',
userId: 1,
},
}),
)
})
it('has two values for the transaction', () => {
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()
})
describe('server error', () => {
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
describe('sever success', () => {
it('sends query to Apollo when created', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
pageSize: 10,
order: 'DESC',
userId: 1,
})
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
})
})
})
})

View File

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

View File

@ -1,17 +1,19 @@
import gql from 'graphql-tag'
export const adminListAllContributions = gql`
export const adminListContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
) {
adminListAllContributions(
adminListContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
userId: $userId
) {
contributionCount
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",
"createdAt": "Created at",
"creation": "Amount",
"creation": "Creation",
"creationList": "Creation list",
"creation_form": {
"creation_for": "Active Basic Income for",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,11 @@ module.exports = {
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 81,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'],
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",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"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",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts",
"locales": "scripts/sort.sh"

View File

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

View File

@ -2,7 +2,6 @@ export enum RIGHTS {
LOGIN = 'LOGIN',
VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE',
GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO',
COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID',
@ -45,10 +44,9 @@ export enum RIGHTS {
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
ADMIN_LIST_CONTRIBUTIONS = 'ADMIN_LIST_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',

View File

@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0060-update_communities_table',
DB_VERSION: '0062-event_contribution_confirm',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// 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,22 @@
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,
amount,
).save()

View File

@ -0,0 +1,22 @@
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,
amount,
).save()

View File

@ -0,0 +1,22 @@
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,
amount,
).save()

View File

@ -0,0 +1,22 @@
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,
amount,
).save()

View File

@ -0,0 +1,22 @@
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,
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,12 @@
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, amount).save()

View File

@ -0,0 +1,12 @@
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, amount).save()

View File

@ -0,0 +1,12 @@
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, 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,22 @@
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,
amount,
).save()

View File

@ -0,0 +1,22 @@
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,
amount,
).save()

View File

@ -1,212 +1,48 @@
import { EventProtocol as DbEvent } from '@entity/EventProtocol'
import { Event as DbEvent } from '@entity/Event'
import { User as DbUser } from '@entity/User'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType'
import { EventType } from './Event'
export const Event = (
type: EventProtocolType,
userId: number,
xUserId: number | null = null,
xCommunityId: number | null = null,
transactionId: number | null = null,
contributionId: number | null = null,
type: EventType,
affectedUser: DbUser,
actingUser: DbUser,
involvedUser: DbUser | null = null,
involvedTransaction: DbTransaction | null = null,
involvedContribution: DbContribution | null = null,
involvedContributionMessage: DbContributionMessage | null = null,
amount: Decimal | null = null,
messageId: number | null = null,
): DbEvent => {
const event = new DbEvent()
event.type = type
event.userId = userId
event.xUserId = xUserId
event.xCommunityId = xCommunityId
event.transactionId = transactionId
event.contributionId = contributionId
event.affectedUser = affectedUser
event.actingUser = actingUser
event.involvedUser = involvedUser
event.involvedTransaction = involvedTransaction
event.involvedContribution = involvedContribution
event.involvedContributionMessage = involvedContributionMessage
event.amount = amount
event.messageId = messageId
return event
}
export const EVENT_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export { EventType } from './EventType'
export const EVENT_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_DELETE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_UPDATE,
userId,
null,
null,
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()
export { EVENT_ACTIVATE_ACCOUNT } from './EVENT_ACTIVATE_ACCOUNT'
export { EVENT_ADMIN_CONTRIBUTION_CONFIRM } from './EVENT_ADMIN_CONTRIBUTION_CONFIRM'
export { EVENT_ADMIN_CONTRIBUTION_CREATE } from './EVENT_ADMIN_CONTRIBUTION_CREATE'
export { EVENT_ADMIN_CONTRIBUTION_DELETE } from './EVENT_ADMIN_CONTRIBUTION_DELETE'
export { EVENT_ADMIN_CONTRIBUTION_DENY } from './EVENT_ADMIN_CONTRIBUTION_DENY'
export { EVENT_ADMIN_CONTRIBUTION_UPDATE } from './EVENT_ADMIN_CONTRIBUTION_UPDATE'
export { EVENT_ADMIN_SEND_CONFIRMATION_EMAIL } from './EVENT_ADMIN_SEND_CONFIRMATION_EMAIL'
export { EVENT_CONTRIBUTION_CREATE } from './EVENT_CONTRIBUTION_CREATE'
export { EVENT_CONTRIBUTION_DELETE } from './EVENT_CONTRIBUTION_DELETE'
export { EVENT_CONTRIBUTION_UPDATE } from './EVENT_CONTRIBUTION_UPDATE'
export { EVENT_LOGIN } from './EVENT_LOGIN'
export { EVENT_REGISTER } from './EVENT_REGISTER'
export { EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL } from './EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL'
export { EVENT_SEND_CONFIRMATION_EMAIL } from './EVENT_SEND_CONFIRMATION_EMAIL'
export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND'
export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE'

View File

@ -1,42 +1,48 @@
export enum EventProtocolType {
// 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',
export { EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL } from './EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL'
export { EVENT_SEND_CONFIRMATION_EMAIL } from './EVENT_SEND_CONFIRMATION_EMAIL'
export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND'
export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE'
export enum EventType {
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
// 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 = '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',
// TODO CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
ADMIN_CONTRIBUTION_CONFIRM = 'ADMIN_CONTRIBUTION_CONFIRM',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
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',
// 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_DENY = 'CONTRIBUTION_DENY',
// CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
// CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
// USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
// ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
// DELETE_USER = 'DELETE_USER',

View File

@ -21,9 +21,13 @@ export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string |
}
}
`
const variables = {}
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)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)

View File

@ -21,9 +21,13 @@ export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string |
}
}
`
const variables = {}
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)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)

View File

@ -1,8 +1,14 @@
import { GraphQLClient } from 'graphql-request'
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 {
private static instance: GraphQLGetClient
private static instanceArray: ClientInstance[] = []
/**
* 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.
*/
public static getInstance(url: string): GraphQLGetClient {
if (!GraphQLGetClient.instance) {
GraphQLGetClient.instance = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url)
if (instance) {
return instance.client
}
return GraphQLGetClient.instance
const client = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
GraphQLGetClient.instanceArray.push({ url, client } as ClientInstance)
return client
}
}

View File

@ -42,17 +42,18 @@ export async function validateCommunities(): Promise<void> {
pubKey,
`${dbCom.endPoint}/${dbCom.apiVersion}`,
)
if (pubKey && pubKey === dbCom.publicKey.toString('hex')) {
if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
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) {
if (!isLogError(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 { Community as DbCommunity } from '@entity/Community'
@ObjectType()
export class Community {
constructor(json?: any) {
if (json) {
this.id = Number(json.id)
this.name = json.name
this.url = json.url
this.description = json.description
this.registerUrl = json.registerUrl
}
constructor(dbCom: DbCommunity) {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString()
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') +
'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)
id: number
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
name: string
publicKey: string
@Field(() => String)
url: string
@Field(() => String)
description: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null
@Field(() => String)
registerUrl: string
@Field(() => Date, { nullable: true })
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-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
jest.mock('@/config')
import { getCommunities } from '@/seeds/graphql/queries'
import { Community as DbCommunity } from '@entity/Community'
import { testEnvironment } from '@test/helpers'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
let testEnv: any
beforeAll(async () => {
const server = await createServer({})
con = server.con
query = createTestClient(server.apollo).query
testEnv = await testEnvironment()
query = testEnv.query
con = testEnv.con
await DbCommunity.clear()
})
afterAll(async () => {
@ -26,74 +27,90 @@ afterAll(async () => {
})
describe('CommunityResolver', () => {
const getCommunityInfoQuery = `
query {
getCommunityInfo {
name
description
url
registerUrl
}
}
`
const communities = `
query {
communities {
id
name
url
description
registerUrl
}
}
`
describe('getCommunityInfo', () => {
it('returns the default values', async () => {
await expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
data: {
getCommunityInfo: {
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register',
describe('getCommunities', () => {
let homeCom1: DbCommunity
let homeCom2: DbCommunity
let homeCom3: DbCommunity
let foreignCom1: DbCommunity
let foreignCom2: DbCommunity
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)
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
getCommunities: [],
},
},
})
})
})
})
describe('communities', () => {
describe('PRODUCTION = false', () => {
beforeEach(() => {
CONFIG.PRODUCTION = false
describe('only home-communities entries', () => {
beforeEach(async () => {
jest.clearAllMocks()
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 () => {
await expect(query({ query: communities })).resolves.toMatchObject({
it('returns three home-community entries', async () => {
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
communities: [
getCommunities: [
{
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register-community',
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,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/',
registerUrl: 'https://stage1.gradido.net/register-community',
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,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
foreign: homeCom3.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/2_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
},
],
},
@ -101,21 +118,104 @@ describe('CommunityResolver', () => {
})
})
describe('PRODUCTION = true', () => {
beforeEach(() => {
CONFIG.PRODUCTION = true
describe('plus foreign-communities entries', () => {
beforeEach(async () => {
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 () => {
await expect(query({ query: communities })).resolves.toMatchObject({
it('returns 3x home and 3x foreign-community entries', async () => {
await expect(query({ query: getCommunities })).resolves.toMatchObject({
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,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
foreign: homeCom3.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/2_0'),
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 { Community } from '@model/Community'
import { Community as DbCommunity } from '@entity/Community'
import { RIGHTS } from '@/auth/RIGHTS'
import CONFIG from '@/config'
@Resolver()
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])
@Query(() => [Community])
communities(): Community[] {
if (CONFIG.PRODUCTION)
return [
new Community({
id: 3,
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',
}),
]
async getCommunities(): Promise<Community[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({
order: { foreign: 'ASC', publicKey: 'ASC', apiVersion: 'ASC' },
})
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
}
}

View File

@ -27,7 +27,7 @@ import {
import {
listAllContributions,
listContributions,
adminListAllContributions,
adminListContributions,
} from '@/seeds/graphql/queries'
import {
sendContributionConfirmedEmail,
@ -46,11 +46,11 @@ import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { EventProtocol } from '@entity/EventProtocol'
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 { EventProtocolType } from '@/event/EventProtocolType'
import { EventType } from '@/event/Event'
import { logger, i18n as localization } from '@test/testSetup'
import { UserInputError } from 'apollo-server-express'
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
@ -279,12 +279,13 @@ describe('ContributionResolver', () => {
})
it('stores the CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
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),
contributionId: pendingContribution.data.createContribution.id,
userId: bibi.id,
}),
)
})
@ -584,12 +585,13 @@ describe('ContributionResolver', () => {
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
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),
contributionId: pendingContribution.data.createContribution.id,
userId: bibi.id,
}),
)
})
@ -814,12 +816,12 @@ describe('ContributionResolver', () => {
})
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({
type: EventProtocolType.ADMIN_CONTRIBUTION_DENY,
userId: bibi.id,
xUserId: admin.id,
contributionId: contributionToDeny.data.createContribution.id,
type: EventType.ADMIN_CONTRIBUTION_DENY,
affectedUserId: bibi.id,
actingUserId: admin.id,
involvedContributionId: contributionToDeny.data.createContribution.id,
amount: expect.decimalEqual(100),
}),
)
@ -942,12 +944,13 @@ describe('ContributionResolver', () => {
})
it('stores the CONTRIBUTION_DELETE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_DELETE,
contributionId: contributionToDelete.data.createContribution.id,
type: EventType.CONTRIBUTION_DELETE,
affectedUserId: bibi.id,
actingUserId: bibi.id,
involvedContributionId: contributionToDelete.data.createContribution.id,
amount: expect.decimalEqual(100),
userId: bibi.id,
}),
)
})
@ -2031,10 +2034,11 @@ describe('ContributionResolver', () => {
})
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({
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
userId: admin.id,
type: EventType.ADMIN_CONTRIBUTION_CREATE,
affectedUserId: bibi.id,
actingUserId: admin.id,
amount: expect.decimalEqual(200),
}),
)
@ -2232,7 +2236,7 @@ describe('ContributionResolver', () => {
mutate({
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
id: creation?.id,
email: 'peter@lustig.de',
amount: new Decimal(300),
memo: 'Danke Peter!',
@ -2256,10 +2260,11 @@ describe('ContributionResolver', () => {
})
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({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
userId: admin.id,
type: EventType.ADMIN_CONTRIBUTION_UPDATE,
affectedUserId: creation?.userId,
actingUserId: admin.id,
amount: 300,
}),
)
@ -2273,7 +2278,7 @@ describe('ContributionResolver', () => {
mutate({
mutation: adminUpdateContribution,
variables: {
id: creation ? creation.id : -1,
id: creation?.id,
email: 'peter@lustig.de',
amount: new Decimal(200),
memo: 'Das war leider zu Viel!',
@ -2297,10 +2302,11 @@ describe('ContributionResolver', () => {
})
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({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
userId: admin.id,
type: EventType.ADMIN_CONTRIBUTION_UPDATE,
affectedUserId: creation?.userId,
actingUserId: admin.id,
amount: expect.decimalEqual(200),
}),
)
@ -2371,7 +2377,7 @@ describe('ContributionResolver', () => {
mutate({
mutation: adminDeleteContribution,
variables: {
id: creation ? creation.id : -1,
id: creation?.id,
},
}),
).resolves.toEqual(
@ -2382,10 +2388,12 @@ describe('ContributionResolver', () => {
})
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({
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
userId: admin.id,
type: EventType.ADMIN_CONTRIBUTION_DELETE,
affectedUserId: creation?.userId,
actingUserId: admin.id,
involvedContributionId: creation?.id,
amount: expect.decimalEqual(200),
}),
)
@ -2538,9 +2546,9 @@ describe('ContributionResolver', () => {
})
it('stores the CONTRIBUTION_CONFIRM event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CONFIRM,
type: EventType.ADMIN_CONTRIBUTION_CONFIRM,
}),
)
})
@ -2570,9 +2578,9 @@ describe('ContributionResolver', () => {
})
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({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
type: EventType.SEND_CONFIRMATION_EMAIL,
}),
)
})
@ -2661,12 +2669,12 @@ describe('ContributionResolver', () => {
})
})
describe('adminListAllContribution', () => {
describe('adminListContributions', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: adminListAllContributions,
query: adminListContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -2691,7 +2699,7 @@ describe('ContributionResolver', () => {
it('returns an error', async () => {
await expect(
query({
query: adminListAllContributions,
query: adminListContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -2715,9 +2723,9 @@ describe('ContributionResolver', () => {
it('returns 17 creations in total', async () => {
const {
data: { adminListAllContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
query: adminListAllContributions,
data: { adminListContributions: contributionListObject },
}: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListContributions,
})
expect(contributionListObject.contributionList).toHaveLength(17)
expect(contributionListObject).toMatchObject({
@ -2882,9 +2890,9 @@ describe('ContributionResolver', () => {
it('returns two pending creations with page size set to 2', async () => {
const {
data: { adminListAllContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
query: adminListAllContributions,
data: { adminListContributions: contributionListObject },
}: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListContributions,
variables: {
currentPage: 1,
pageSize: 2,

View File

@ -42,7 +42,7 @@ import {
EVENT_ADMIN_CONTRIBUTION_CREATE,
EVENT_ADMIN_CONTRIBUTION_UPDATE,
EVENT_ADMIN_CONTRIBUTION_DELETE,
EVENT_CONTRIBUTION_CONFIRM,
EVENT_ADMIN_CONTRIBUTION_CONFIRM,
EVENT_ADMIN_CONTRIBUTION_DENY,
} from '@/event/Event'
import { calculateDecay } from '@/util/decay'
@ -91,7 +91,7 @@ export class ContributionResolver {
logger.trace('contribution to save', contribution)
await DbContribution.save(contribution)
await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
await EVENT_CONTRIBUTION_CREATE(user, contribution, amount)
return new UnconfirmedContribution(contribution, user, creations)
}
@ -119,7 +119,7 @@ export class ContributionResolver {
contribution.deletedAt = new Date()
await contribution.save()
await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
await EVENT_CONTRIBUTION_DELETE(user, contribution, contribution.amount)
const res = await contribution.softRemove()
return !!res
@ -136,15 +136,15 @@ export class ContributionResolver {
): Promise<ContributionListResult> {
const user = getUser(context)
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
true,
['messages'],
user.id,
withDeleted: true,
relations: ['messages'],
userId: user.id,
statusFilter,
)
})
return new ContributionListResult(
count,
dbContributions.map((contribution) => new Contribution(contribution, user)),
@ -159,15 +159,13 @@ export class ContributionResolver {
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
false,
['user'],
undefined,
relations: ['user'],
statusFilter,
)
})
return new ContributionListResult(
count,
@ -249,7 +247,7 @@ export class ContributionResolver {
contributionToUpdate.updatedAt = new Date()
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)
}
@ -306,7 +304,7 @@ export class ContributionResolver {
await DbContribution.save(contribution)
await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
await EVENT_ADMIN_CONTRIBUTION_CREATE(emailContact.user, moderator, contribution, amount)
return getUserCreation(emailContact.userId, clientTimezoneOffset)
}
@ -374,28 +372,35 @@ export class ContributionResolver {
result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount)
await EVENT_ADMIN_CONTRIBUTION_UPDATE(
emailContact.user,
moderator,
contributionToUpdate,
amount,
)
return result
}
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => ContributionListResult) // [UnconfirmedContribution]
async adminListAllContributions(
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async adminListContributions(
@Args()
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
@Arg('userId', () => Int, { nullable: true })
userId?: number | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
true,
['user', 'messages'],
undefined,
withDeleted: true,
userId,
relations: ['user', 'messages'],
statusFilter,
)
})
return new ContributionListResult(
count,
@ -432,7 +437,12 @@ export class ContributionResolver {
await contribution.save()
const res = await contribution.softRemove()
await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount)
await EVENT_ADMIN_CONTRIBUTION_DELETE(
{ id: contribution.userId } as DbUser,
moderator,
contribution,
contribution.amount,
)
void sendContributionDeletedEmail({
firstName: user.firstName,
@ -545,40 +555,13 @@ export class ContributionResolver {
await queryRunner.release()
}
await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
await EVENT_ADMIN_CONTRIBUTION_CONFIRM(user, moderatorUser, contribution, contribution.amount)
} finally {
releaseLock()
}
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])
@Query(() => [OpenCreation])
async openCreations(@Ctx() context: Context): Promise<OpenCreation[]> {
@ -632,9 +615,9 @@ export class ContributionResolver {
const res = await contributionToUpdate.save()
await EVENT_ADMIN_CONTRIBUTION_DENY(
contributionToUpdate.userId,
moderator.id,
contributionToUpdate.id,
user,
moderator,
contributionToUpdate,
contributionToUpdate.amount,
)

View File

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

View File

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

View File

@ -45,8 +45,8 @@ import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
import { ContributionLink } from '@model/ContributionLink'
import { TransactionLink } from '@entity/TransactionLink'
import { EventProtocolType } from '@/event/EventProtocolType'
import { EventProtocol } from '@entity/EventProtocol'
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 { UserContact } from '@entity/UserContact'
@ -193,10 +193,11 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REGISTER,
userId: userConatct.user.id,
type: EventType.REGISTER,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}),
)
})
@ -222,10 +223,11 @@ describe('UserResolver', () => {
})
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({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
userId: user[0].id,
type: EventType.SEND_CONFIRMATION_EMAIL,
affectedUserId: user[0].id,
actingUserId: user[0].id,
}),
)
})
@ -267,10 +269,11 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
userId: userConatct.user.id,
type: EventType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
affectedUserId: userConatct.user.id,
actingUserId: 0,
}),
)
})
@ -367,20 +370,22 @@ describe('UserResolver', () => {
})
it('stores the ACTIVATE_ACCOUNT event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT,
userId: user[0].id,
type: EventType.ACTIVATE_ACCOUNT,
affectedUserId: user[0].id,
actingUserId: user[0].id,
}),
)
})
it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER,
userId: result.data.createUser.id,
contributionId: link.id,
type: EventType.REDEEM_REGISTER,
affectedUserId: result.data.createUser.id,
actingUserId: result.data.createUser.id,
involvedContributionId: link.id,
}),
)
})
@ -460,10 +465,12 @@ describe('UserResolver', () => {
})
it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER,
userId: newUser.data.createUser.id,
type: EventType.REDEEM_REGISTER,
affectedUserId: newUser.data.createUser.id,
actingUserId: newUser.data.createUser.id,
involvedTransactionId: transactionLink.id,
}),
)
})
@ -691,10 +698,11 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
userId: userConatct.user.id,
type: EventType.LOGIN,
affectedUserId: userConatct.user.id,
actingUserId: userConatct.user.id,
}),
)
})
@ -939,10 +947,11 @@ describe('UserResolver', () => {
})
it('stores the LOGIN event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
userId: user[0].id,
type: EventType.LOGIN,
affectedUserId: user[0].id,
actingUserId: user[0].id,
}),
)
})
@ -1858,10 +1867,11 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
await expect(EventProtocol.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL,
userId: userConatct.user.id,
type: EventType.ADMIN_SEND_CONFIRMATION_EMAIL,
affectedUserId: userConatct.user.id,
actingUserId: admin.id,
}),
)
})

View File

@ -20,7 +20,9 @@ import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeor
import { User as DbUser } from '@entity/User'
import { UserContact as DbUserContact } from '@entity/UserContact'
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { Contribution as DbContribution } from '@entity/Contribution'
import { UserRepository } from '@repository/User'
import { User } from '@model/User'
@ -54,6 +56,7 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys'
import {
Event,
EventType,
EVENT_LOGIN,
EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
EVENT_SEND_CONFIRMATION_EMAIL,
@ -67,7 +70,6 @@ import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
import LogError from '@/server/LogError'
import { EventProtocolType } from '@/event/EventProtocolType'
import { findUserByIdentifier } from './util/findUserByIdentifier'
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -183,7 +185,7 @@ export class UserResolver {
value: encode(dbUser.gradidoID),
})
await EVENT_LOGIN(user.id)
await EVENT_LOGIN(dbUser)
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user
}
@ -253,7 +255,7 @@ export class UserResolver {
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(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
@ -271,7 +273,11 @@ export class UserResolver {
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()
dbUser.gradidoID = gradidoID
dbUser.firstName = firstName
@ -288,14 +294,16 @@ export class UserResolver {
logger.info('redeemCode found contributionLink', contributionLink)
if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id
eventRegisterRedeem.contributionId = contributionLink.id
// TODO this is so wrong
eventRegisterRedeem.involvedContribution = { id: contributionLink.id } as DbContribution
}
} else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink', transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
eventRegisterRedeem.transactionId = transactionLink.id
// TODO this is so wrong
eventRegisterRedeem.involvedTransaction = { id: transactionLink.id } as DbTransaction
}
}
}
@ -334,7 +342,7 @@ export class UserResolver {
})
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
await EVENT_SEND_CONFIRMATION_EMAIL(dbUser.id)
await EVENT_SEND_CONFIRMATION_EMAIL(dbUser)
if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`)
@ -351,10 +359,11 @@ export class UserResolver {
logger.info('createUser() successful...')
if (redeemCode) {
eventRegisterRedeem.userId = dbUser.id
eventRegisterRedeem.affectedUser = dbUser
eventRegisterRedeem.actingUser = dbUser
await eventRegisterRedeem.save()
} else {
await EVENT_REGISTER(dbUser.id)
await EVENT_REGISTER(dbUser)
}
return new User(dbUser)
@ -470,7 +479,7 @@ export class UserResolver {
await queryRunner.commitTransaction()
logger.info('User and UserContact data written successfully...')
await EVENT_ACTIVATE_ACCOUNT(user.id)
await EVENT_ACTIVATE_ACCOUNT(user)
} catch (e) {
await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e)
@ -780,9 +789,13 @@ export class UserResolver {
return null
}
// TODO this is an admin function - needs refactor
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@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()
// const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email)
@ -807,7 +820,7 @@ export class UserResolver {
if (!emailSent) {
logger.info(`Account confirmation link: ${activationLink}`)
} else {
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id)
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user, getUser(context))
}
return true

View File

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

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`
query ($code: String!) {
queryTransactionLink(code: $code) {
@ -204,18 +220,20 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
`
// from admin interface
export const adminListAllContributions = gql`
export const adminListContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
) {
adminListAllContributions(
adminListContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
userId: $userId
) {
contributionCount
contributionList {

View File

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

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

@ -0,0 +1 @@
export { Event } from './0061-event_refactoring/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 { UserContact } from './UserContact'
import { Contribution } from './Contribution'
import { EventProtocol } from './EventProtocol'
import { Event } from './Event'
import { ContributionMessage } from './ContributionMessage'
import { Community } from './Community'
export const entities = [
Community,
Contribution,
ContributionLink,
ContributionMessage,
Event,
LoginElopageBuys,
LoginEmailOptIn,
Migration,
Transaction,
TransactionLink,
User,
EventProtocol,
ContributionMessage,
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

@ -16,9 +16,7 @@
"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_reset": "cross-env TZ=UTC ts-node src/index.ts reset",
"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"
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
"devDependencies": {
"@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
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_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

@ -42,19 +42,19 @@ server {
# Frontend (default)
location / {
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_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:3000;
proxy_redirect off;
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
}
}
# Backend
location /graphql {
@ -112,6 +112,9 @@ server {
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
}
# Federation
$FEDERATION_NGINX_CONF
# TODO this could be a performance optimization
#location /vue {
# alias /var/www/html/gradido/frontend/dist;

View File

@ -27,19 +27,19 @@ server {
# Frontend (default)
location / {
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_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:3000;
proxy_redirect off;
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
access_log $GRADIDO_LOG_PATH/nginx-access.frontend.log gradido_log;
error_log $GRADIDO_LOG_PATH/nginx-error.frontend.log warn;
}
}
# Backend
location /graphql {
@ -98,6 +98,9 @@ server {
error_log $GRADIDO_LOG_PATH/nginx-error.admin.log warn;
}
# Federation
$FEDERATION_NGINX_CONF
# TODO this could be a performance optimization
#location /vue {
# 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
# stop all services
echo 'Stopping all Gradido services' >> $UPDATE_HTML
pm2 stop all
echo 'Stop and delete all Gradido services' >> $UPDATE_HTML
pm2 delete all
pm2 save
# git
BRANCH=${1:-master}
@ -73,12 +74,41 @@ git pull
export BUILD_COMMIT="$(git rev-parse HEAD)"
# 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
case "$NGINX_SSL" in
true) TEMPLATE_FILE="gradido.conf.ssl.template" ;;
*) TEMPLATE_FILE="gradido.conf.template" ;;
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
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/admin/.env $PROJECT_ROOT/admin/.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/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/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/federation/.env.template > $PROJECT_ROOT/federation/.env
# Install & build database
echo 'Updating database' >> $UPDATE_HTML
@ -124,7 +156,6 @@ if [ "$DEPLOY_SEED_DATA" = "true" ]; then
fi
# TODO maybe handle this differently?
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 save
@ -137,7 +168,6 @@ yarn install
yarn build
# TODO maybe handle this differently?
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 save
@ -150,7 +180,6 @@ yarn install
yarn build
# TODO maybe handle this differently?
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 save
@ -163,15 +192,49 @@ yarn install
yarn build
# TODO maybe handle this differently?
export NODE_ENV=production
pm2 delete gradido-dht-node
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 save
else
echo "====================================================================="
echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..."
echo "====================================================================="
echo "=====================================================================" >> $UPDATE_HTML
echo "WARNING: FEDERATION_DHT_TOPIC not configured. DHT-Node not started..." >> $UPDATE_HTML
echo "=====================================================================" >> $UPDATE_HTML
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
echo 'Configuring nginx to serve gradido again' >> $UPDATE_HTML

View File

@ -4,6 +4,11 @@ module.exports = {
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 80,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: [],
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",
"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 .",
"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": {
"@hyperswarm/dht": "^6.4.4",

View File

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

View File

@ -719,11 +719,11 @@ describe('federation', () => {
JSON.stringify([
{
api: '1_0',
url: 'http://localhost:5001/api/',
url: 'http://localhost/api/',
},
{
api: '2_0',
url: 'http://localhost:5002/api/',
url: 'http://localhost/api/',
},
]),
),
@ -747,7 +747,7 @@ describe('federation', () => {
foreign: true,
publicKey: expect.any(Buffer),
apiVersion: '1_0',
endPoint: 'http://localhost:5001/api/',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
@ -764,7 +764,7 @@ describe('federation', () => {
foreign: true,
publicKey: expect.any(Buffer),
apiVersion: '2_0',
endPoint: 'http://localhost:5002/api/',
endPoint: 'http://localhost/api/',
lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date),
updatedAt: null,
@ -786,15 +786,15 @@ describe('federation', () => {
JSON.stringify([
{
api: '1_0',
url: 'http://localhost:5001/api/',
url: 'http://localhost/api/',
},
{
api: '1_1',
url: 'http://localhost:5002/api/',
url: 'http://localhost/api/',
},
{
api: '2_0',
url: 'http://localhost:5003/api/',
url: 'http://localhost/api/',
},
]),
),

View File

@ -181,11 +181,9 @@ export const startDHT = async (topic: string): Promise<void> => {
async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const port =
Number.parseInt(CONFIG.FEDERATION_COMMUNITY_API_PORT) + Number(apiEnum.replace('_', ''))
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL + ':' + port.toString() + '/api/',
url: CONFIG.FEDERATION_COMMUNITY_URL + '/api/',
}
return comApi
})

View File

@ -4,29 +4,6 @@ This document contains the concept and technical details for the *federation* of
But meanwhile the usage of a DHT like HyperSwarm promises more coverage of the gradido requirements out of the box. More details about HyperSwarm can be found here [@hyperswarm/dht](https://github.com/hyperswarm/dht).
## ActivityPub (deprecated)
The activity pub defines a server-to-server federation protocol to share information between decentralized instances and will be the main komponent for the gradido community federation.
At first we asume a *gradido community* as an *ActivityPub user*. A user is represented by "*actors*" via the users's accounts on servers. User's accounts on different servers corrsponds to different actors, which means community accounts on different servers corrsponds to different communities.
Every community (actor) has an:
* inbox: to get messages from the world
* outbox: to send messages to others
and are simple endpoints or just URLs, which are described in the *ActivityStream* of each *ActivityPub community*.
### Open Decision:
It has to be decided, if the Federation will work with an internal or with external ActivityPub-Server, as shown in the picture below:
![FederationActivityPub](./image/FederationActivityPub.png " ")
The Variant A with an internal server contains the benefit to be as independent as possible from third party service providers and will not cause additional hosting costs. But this solution will cause the additional efforts of impementing an ActivityPub-Server in the gradido application and the responsibility for this component.
The Varaint B with an external server contains the benefit to reduce the implementation efforts and the responsibility for an own ActivitPub-Server. But it will cause an additional dependency to a third party service provider and the growing hosting costs.
## HyperSwarm
The decision to switch from ActivityPub to HyperSwarm base on the arguments, that the *hyperswarm/dht* library will satify the most federation requirements out of the box. It is now to design the business requirements of the [gradido community communication](../BusinessRequirements/CommunityVerwaltung.md#UC-createCommunity) in a technical conception.
@ -41,12 +18,30 @@ To enable such a relationship between an existing community and a new community
2. Authentication
3. Autorized Communication
### Overview
### Overview of Federation-Handshake
At first the following diagramm gives an overview of the three stages and shows the handshake between an existing community-A and a new created community-B including the data exchange for buildup such a federated, authenticated and autorized relationship.
![FederationHyperSwarm.png](./image/FederationHyperSwarm.png)
### Technical Architecture
The previous described handshake will be done by several technical modules of the gradido system. The following picture gives an overview about the modules and how the communicate with each other.
![img](./image/TechnicalOverview_V1-19.svg)
As soon as a Gradido Community is up and running the DHT-Modul first writes the home-community-entries in the database and starts with the federation via HyperSwarm. Each community, which is configured with the configuration key GRADIDO_HUB to listen on the correct topic of the DHT will be part of the Gradido-Net-Federation. That means each DHT-Modul of each community will receive the publicKey of all connected communities. The DHT-Modul will open for each received publicKey a communication-socket with the associated community DHT-Modul. Over this open socket the connected communities exchange the data "api-version" and "url" for later direct communication between both communities. The exchanged api-version info and urls will be written in the own database.
The background of this exchanged data base on the supported api-versions a community will support with its own federation-modules. Each running federation-module in a community will support exact one graphql api-version of a cross-community-communication. To reach a dedicated federation-module with the correct api-version during a cross-community-communication the url for this federation-module must be known by both communities. As shown in the picture above the graphql-client with api-version V1_0 in the left community will interact with the federation-module with api-version V1_0 on the right community. During the lifecycle of the gradido-application it will be necessary to extent the features and interfaces for the cross-community-communication. To keep a backwards compatibilty and not to force each community to always upgrade their running software version on the last api-version at the same time, it will be necessary to support several api-versions in parallel. The different running api-version modules are responsible to convert and treat the exchanged data in a correct way to ensure konsistent data in the local database of the community.
The up and running Backend-Module contains a validation logic to verify the community entries from the own DHT-Module. For each announced but unverified community-entry the GraphQL-Client is used to invoke a getPublicKey-Request. Depending on the containing api-version the matching GraphQL-Client is used and the getPublicKey-Request will be send to the given URL.
As soon as the federation-module of the associated community received the getPublicKey-request the own publicKey is read from database and send back in the response.
The GraphQL-Client will read the publicKey of the other community from the returned response data and compare it with the data of the community-entry, which caused the getPublicKey-Request. If they match the community-entry will be updated by inserting the current timestamp in the verifiedAt-field of this community-entry.
This federation and verification logic will work the whole time and can be monitored by observing the communities-table changes. The Admin-UI will contain a Page to have a look on the current state of the communities table content.
### Prerequisits
Before starting in describing the details of the federation handshake, some prerequisits have to be defined.
@ -235,7 +230,6 @@ For the first federation release the *DHT-Node* will be part of the *apollo serv
| communityApiVersion.apiversion | keep existing value |
| communityApiVersion.validFrom | exchangedData.API.validFrom |
| communityApiVersion.verifiedAt | keep existing value |
*
3. After all received data is stored successfully, the *DHT-Node* starts the *stage2 - Authentication* of the federation handshake
### Stage2 - Authentication
@ -284,8 +278,6 @@ As soon the *openConnection* request is invoked:
3. check if the decrypted `parameter.signedAndEncryptedURL` is equals the selected url from the previous selected CommunityFederationEntry
1. if not then break the further processing of this request by only writing an error-log event. There will be no answer to the invoker community, because this community will only go on with a `openConnectionRedirect`-request from this community.
2. if yes then verify the signature of `parameter.signedAndEncryptedURL` with the `cf.pubKey` read in step 2 before
3.
4.
### Stage3 - Autorized Business Communication

View File

@ -0,0 +1,282 @@
<mxfile host="65bd71144e">
<diagram id="RqE3izjX3TYt3HTUOB95" name="Seite-1">
<mxGraphModel dx="3343" dy="773" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="2336" pageHeight="1654" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="Community&amp;nbsp; &quot;Gradido-Akademie&quot;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#dae8fc;strokeColor=#6c8ebf;verticalAlign=top;fontSize=16;fontStyle=1;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-158.26" y="80" width="870" height="800" as="geometry"/>
</mxCell>
<mxCell id="3" value="Gradido - technical Infrastructure-Overview&lt;br&gt;&lt;font style=&quot;font-size: 12px&quot;&gt;State of 02.2023&lt;/font&gt;" style="text;html=1;strokeColor=#82b366;fillColor=#d5e8d4;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=16;fontStyle=1;labelBorderColor=none;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1160" y="20" width="1880" height="40" as="geometry"/>
</mxCell>
<mxCell id="4" value="&lt;b&gt;Backend-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="-118.25999999999999" y="269" width="540" height="101" as="geometry"/>
</mxCell>
<mxCell id="7" value="CommunityServer DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#dae8fc;strokeColor=#6c8ebf;fontSize=15;fontStyle=1;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="211.74" y="760" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="8" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.75;entryY=1;entryDx=0;entryDy=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="7" target="4" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="191.74" y="590" as="sourcePoint"/>
<mxPoint x="241.74" y="540" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="15" value="&lt;b&gt;Layer 1:&lt;/b&gt;" style="text;html=1;strokeColor=#6c8ebf;fillColor=#dae8fc;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;opacity=0;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-148.26" y="735" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="16" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;entryX=1.002;entryY=0.801;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="2" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-158.26" y="720" as="sourcePoint"/>
<mxPoint x="603.74" y="720" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="17" value="&lt;b&gt;Layer 2:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-148.26" y="240" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="21" value="&quot;&lt;b&gt;GDT-Server&lt;/b&gt;&quot; &lt;br&gt;base on cakephp + mySQL" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#fff2cc;strokeColor=#d6b656;gradientColor=#ffd966;" parent="1" vertex="1">
<mxGeometry x="491.74" y="269" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="22" value="GDT-Server DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#fff2cc;strokeColor=#d6b656;fontSize=15;fontStyle=1;gradientColor=#ffd966;" parent="1" vertex="1">
<mxGeometry x="521.74" y="760" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="23" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="22" target="21" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="381.74" y="590" as="sourcePoint"/>
<mxPoint x="586.6200000000001" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="24" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="1" target="21" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="421.74" y="299" as="sourcePoint"/>
<mxPoint x="401.74" y="479" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="25" value="&lt;font style=&quot;font-size: 12px&quot;&gt;json-&lt;br&gt;ajax-&lt;br&gt;request&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="24" vertex="1" connectable="0">
<mxGeometry x="-0.3429" relative="1" as="geometry">
<mxPoint x="10" y="-28" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="30" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;entryX=1.002;entryY=0.401;entryDx=0;entryDy=0;entryPerimeter=0;exitX=0;exitY=0.4;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-159.9999999999999" y="230" as="sourcePoint"/>
<mxPoint x="711.7399999999999" y="230.79999999999995" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="31" value="&lt;b&gt;Layer 3:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-148.26" y="90" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="32" value="&quot;&lt;b&gt;Elopage&lt;/b&gt;&quot; &lt;br&gt;external Service-Portal" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#fff2cc;strokeColor=#d6b656;" parent="1" vertex="1">
<mxGeometry x="471.74" y="120" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="33" value="&quot;&lt;b&gt;User-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-48.25999999999999" y="120" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="34" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.315;exitY=-0.05;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="4" target="33" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="351.74" y="420" as="sourcePoint"/>
<mxPoint x="401.74" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="35" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.395;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="21" target="32" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="576.24" y="310" as="sourcePoint"/>
<mxPoint x="576.24" y="180" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="36" value="&lt;span style=&quot;font-size: 12px&quot;&gt;graphql&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="1" vertex="1" connectable="0">
<mxGeometry x="191.74" y="216" as="geometry">
<mxPoint x="-169" y="-4" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="37" value="&lt;font style=&quot;font-size: 12px&quot;&gt;json-&lt;br&gt;request&lt;/font&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="1" vertex="1" connectable="0">
<mxGeometry x="551.74" y="216" as="geometry">
<mxPoint x="-2" y="-15" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="39" style="edgeStyle=none;html=1;entryX=0.767;entryY=-0.017;entryDx=0;entryDy=0;startArrow=classic;startFill=1;entryPerimeter=0;" parent="1" source="38" target="4" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="40" value="graphql" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" parent="39" vertex="1" connectable="0">
<mxGeometry x="-0.4305" y="1" relative="1" as="geometry">
<mxPoint x="23" y="-1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="38" value="&quot;&lt;b&gt;Admin-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="191.74" y="120" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="42" style="edgeStyle=none;html=1;entryX=0.145;entryY=0;entryDx=0;entryDy=4.35;entryPerimeter=0;startArrow=classic;startFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="41" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="41" value="&lt;b&gt;DHT-Modul&lt;/b&gt;&lt;br&gt;HyperSwarm" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-118.25999999999999" y="640" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="102" style="edgeStyle=none;html=1;fontSize=16;startArrow=classic;startFill=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="43" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="260" y="760" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="43" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V2_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-118.25999999999999" y="380" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="101" style="edgeStyle=none;html=1;fontSize=16;startArrow=classic;startFill=1;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="44" edge="1">
<mxGeometry relative="1" as="geometry">
<mxPoint x="260" y="760" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="44" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_x" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-88.25999999999999" y="430" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="100" style="edgeStyle=none;html=1;entryX=0.335;entryY=0.013;entryDx=0;entryDy=0;entryPerimeter=0;fontSize=16;startArrow=classic;startFill=1;exitX=0.78;exitY=0.667;exitDx=0;exitDy=0;exitPerimeter=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="45" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="45" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-58.25999999999999" y="480" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="47" style="edgeStyle=none;html=1;entryX=0.333;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0.996;exitY=0.633;exitDx=0;exitDy=0;exitPerimeter=0;fillColor=#dae8fc;gradientColor=#7ea6e0;strokeColor=#6c8ebf;" parent="1" source="46" target="7" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="46" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-28.25999999999999" y="530" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="48" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-108.25999999999999" y="289" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="49" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-98.25999999999999" y="299" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="50" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-88.25999999999999" y="309" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="51" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#dae8fc;strokeColor=#6c8ebf;gradientColor=#7ea6e0;" parent="1" vertex="1">
<mxGeometry x="-78.25999999999999" y="319" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="52" value="Community &quot;GallischesDorf-TBB&quot;" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;verticalAlign=top;fontStyle=1;fontSize=16;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1148.26" y="80" width="628.26" height="800" as="geometry"/>
</mxCell>
<mxCell id="54" value="&lt;b&gt;Backend-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;verticalAlign=top;" parent="1" vertex="1">
<mxGeometry x="-1108.26" y="269" width="568.26" height="101" as="geometry"/>
</mxCell>
<mxCell id="55" value="CommunityServer DB" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;fillColor=#d5e8d4;strokeColor=#82b366;fontSize=15;fontStyle=1;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1048.26" y="770" width="150" height="80" as="geometry"/>
</mxCell>
<mxCell id="56" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.25;entryY=1;entryDx=0;entryDy=0;" parent="1" source="55" target="54" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-798.26" y="590" as="sourcePoint"/>
<mxPoint x="-748.26" y="540" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="57" value="&lt;b&gt;Layer 1:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-1138.26" y="735" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="58" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;entryX=1.002;entryY=0.801;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" target="52" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-1148.26" y="720" as="sourcePoint"/>
<mxPoint x="-386.26" y="720" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="59" value="&lt;b&gt;Layer 2:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-1138.26" y="240" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="65" value="" style="endArrow=none;dashed=1;html=1;fontSize=15;fontColor=#000000;exitX=0;exitY=0.4;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-1149.9999999999998" y="230" as="sourcePoint"/>
<mxPoint x="-520" y="231" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="66" value="&lt;b&gt;Layer 3:&lt;/b&gt;" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;glass=0;labelBackgroundColor=none;fontSize=15;fontColor=#000000;opacity=0;" parent="1" vertex="1">
<mxGeometry x="-1138.26" y="90" width="70" height="20" as="geometry"/>
</mxCell>
<mxCell id="68" value="&quot;&lt;b&gt;User-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-1030" y="120" width="200" height="60" as="geometry"/>
</mxCell>
<mxCell id="69" value="" style="endArrow=classic;startArrow=classic;html=1;fontSize=15;fontColor=#000000;entryX=0.5;entryY=1;entryDx=0;entryDy=0;exitX=0.315;exitY=-0.05;exitDx=0;exitDy=0;exitPerimeter=0;" parent="1" source="54" target="68" edge="1">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="-638.26" y="420" as="sourcePoint"/>
<mxPoint x="-588.26" y="370" as="targetPoint"/>
</mxGeometry>
</mxCell>
<mxCell id="71" value="&lt;span style=&quot;font-size: 12px&quot;&gt;graphql&lt;/span&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=15;fontColor=#000000;labelBackgroundColor=none;" parent="1" vertex="1" connectable="0">
<mxGeometry x="-798.26" y="216" as="geometry">
<mxPoint x="-169" y="-4" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="73" style="edgeStyle=none;html=1;entryX=0.767;entryY=-0.017;entryDx=0;entryDy=0;startArrow=classic;startFill=1;entryPerimeter=0;" parent="1" source="75" target="54" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="74" value="graphql" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=none;" parent="73" vertex="1" connectable="0">
<mxGeometry x="-0.4305" y="1" relative="1" as="geometry">
<mxPoint x="23" y="-1" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="75" value="&quot;&lt;b&gt;Admin-UI&lt;/b&gt;&quot;" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-775" y="120" width="210" height="60" as="geometry"/>
</mxCell>
<mxCell id="76" style="edgeStyle=none;html=1;entryX=0.855;entryY=0;entryDx=0;entryDy=4.35;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="77" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="90" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;startArrow=classic;startFill=1;" parent="1" source="77" target="41" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="91" value="&amp;nbsp;DHT-Socket Communication&amp;nbsp;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="90" vertex="1" connectable="0">
<mxGeometry x="-0.1307" y="-1" relative="1" as="geometry">
<mxPoint as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="77" value="&lt;b&gt;DHT-Modul&lt;/b&gt;&lt;br&gt;HyperSwarm" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-745" y="640" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="87" style="edgeStyle=none;html=1;entryX=0.655;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="80" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="80" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;dashed=1;" parent="1" vertex="1">
<mxGeometry x="-760" y="500" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="81" style="edgeStyle=none;html=1;entryX=0.655;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;startArrow=classic;startFill=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;" parent="1" source="82" target="55" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="82" value="&lt;b&gt;Federation-Modul&lt;/b&gt;&lt;br&gt;GraphQL-API V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-730" y="550" width="190" height="60" as="geometry"/>
</mxCell>
<mxCell id="98" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="85" target="45" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="99" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="98" vertex="1" connectable="0">
<mxGeometry x="-0.5775" relative="1" as="geometry">
<mxPoint x="28" y="-14" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="85" value="GraphQL-Client V1_1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-745" y="289" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="96" style="edgeStyle=none;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="86" target="46" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="97" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="96" vertex="1" connectable="0">
<mxGeometry x="-0.5322" y="-1" relative="1" as="geometry">
<mxPoint x="-11" y="16" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="86" value="GraphQL-Client V1_0" style="rounded=1;whiteSpace=wrap;html=1;fontSize=16;align=center;fillColor=#d5e8d4;strokeColor=#82b366;gradientColor=#97d077;" parent="1" vertex="1">
<mxGeometry x="-730" y="314" width="180" height="31" as="geometry"/>
</mxCell>
<mxCell id="92" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="51" target="82" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="94" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="92" vertex="1" connectable="0">
<mxGeometry x="0.1267" y="2" relative="1" as="geometry">
<mxPoint x="-93" y="56" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="93" style="edgeStyle=none;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;fontSize=16;startArrow=classic;startFill=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;" parent="1" source="50" target="80" edge="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="95" value="graphQL-Handshake" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];fontSize=16;" parent="93" vertex="1" connectable="0">
<mxGeometry x="-0.0706" relative="1" as="geometry">
<mxPoint x="63" y="-60" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -56,7 +56,7 @@ export default defineConfig({
env: {
backendURL: 'http://localhost:4000',
mailserverURL: 'http://localhost:1080',
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
loginQuery: `mutation ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
email
firstName
@ -69,7 +69,8 @@ export default defineConfig({
hasElopage
publisherId
isAdmin
creation
hideAmountGDD
hideAmountGDT
__typename
}
}`,

View File

@ -5,13 +5,13 @@ Feature: User Authentication - reset password
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
# Background:
# Given the following "users" are in the database:
# | email | password | name |
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
# | email | password | name |
# | raeuber@hotzenplotz.de | Aa12345_ | Räuber Hotzenplotz |
Scenario: Reset password from signin page successfully
Given the user navigates to page "/login"
And the user navigates to the forgot password page
When the user enters the e-mail address "bibi@bloxberg.de"
When the user enters the e-mail address "raeuber@hotzenplotz.de"
And the user submits the e-mail form
Then the user receives an e-mail containing the "password reset" link
When the user opens the "password reset" link in the browser
@ -19,7 +19,7 @@ Feature: User Authentication - reset password
And the user repeats the password "12345Aa_"
And the user submits the password form
And the user clicks the sign in button
Then the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
Then the user submits the credentials "raeuber@hotzenplotz.de" "Aa12345_"
And the user cannot login
But the user submits the credentials "bibi@bloxberg.de" "12345Aa_"
And the user is logged in with username "Bibi Bloxberg"
But the user submits the credentials "raeuber@hotzenplotz.de" "12345Aa_"
And the user is logged in with username "Räuber Hotzenplotz"

View File

@ -5,14 +5,14 @@ Feature: User profile - change password
Background:
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
# Given the following "users" are in the database:
# | email | password | name |
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg | |
# | email | password | name |
# | bob@baumeister.de | Aa12345_ | Bob der Baumeister |
# TODO instead of credentials use the name of an user object (see seeds in backend)
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
Given the user is logged in as "bob@baumeister.de" "Aa12345_"
Scenario: Change password successfully
Given the user navigates to page "/profile"
Given the user navigates to page "/settings"
And the user opens the change password menu
When the user fills the password form with:
| Old password | Aa12345_ |
@ -21,7 +21,7 @@ Feature: User profile - change password
And the user submits the password form
And the user is presented a "success" message
And the user logs out
Then the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
Then the user submits the credentials "bob@baumeister.de" "Aa12345_"
And the user cannot login
But the user submits the credentials "bibi@bloxberg.de" "12345Aa_"
And the user is logged in with username "Bibi Bloxberg"
But the user submits the credentials "bob@baumeister.de" "12345Aa_"
And the user is logged in with username "Bob der Baumeister"

View File

@ -11,7 +11,7 @@ export class SideNavMenu {
}
logout() {
cy.get(this.logoutMenu).click()
cy.get('.main-sidebar').find(this.logoutMenu).click()
return this
}
}

11
federation/.env.dist Normal file
View File

@ -0,0 +1,11 @@
# Database
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=gradido_community
DB_USER=root
DB_PASSWORD=
# Federation
FEDERATION_API=1_0
FEDERATION_PORT=5010
FEDERATION_COMMUNITY_URL=http://localhost

16
federation/.env.template Normal file
View File

@ -0,0 +1,16 @@
CONFIG_VERSION=$FEDERATION_CONFIG_VERSION
LOG_LEVEL=$LOG_LEVEL
# this is set fix to false, because it is important for 'production' environments. only set to true if a graphql-playground should be in use
GRAPHIQL=false
# Database
DB_HOST=$DB_HOST
DB_PORT=$DB_PORT
DB_DATABASE=$DB_DATABASE
DB_USER=$DB_USER
DB_PASSWORD=$DB_PASSWORD
# Federation
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL

View File

@ -9,6 +9,11 @@ module.exports = {
'!src/seeds/**',
'!build/**',
],
coverageThreshold: {
global: {
lines: 72,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: [],
modulePathIgnorePatterns: ['<rootDir>/build/'],

View File

@ -11,7 +11,7 @@
"build": "tsc --build",
"clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"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",
"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 ."
},

View File

@ -11,7 +11,7 @@ Decimal.set({
*/
const constants = {
DB_VERSION: '0060-update_communities_table',
DB_VERSION: '0062-event_contribution_confirm',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
@ -24,7 +24,6 @@ const constants = {
}
const server = {
PORT: process.env.PORT || 5010,
// JWT_SECRET: process.env.JWT_SECRET || 'secret123',
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
@ -40,21 +39,6 @@ const database = {
TYPEORM_LOGGING_RELATIVE_PATH:
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.backend.log',
}
/*
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME || 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL || 'http://localhost/',
COMMUNITY_REGISTER_URL: process.env.COMMUNITY_REGISTER_URL || 'http://localhost/register',
COMMUNITY_REDEEM_URL: process.env.COMMUNITY_REDEEM_URL || 'http://localhost/redeem/{code}',
COMMUNITY_REDEEM_CONTRIBUTION_URL:
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
COMMUNITY_DESCRIPTION:
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
}
*/
// This is needed by graphql-directive-auth
// process.env.APP_SECRET = server.JWT_SECRET
// Check config version
constants.CONFIG_VERSION.CURRENT =
@ -71,10 +55,8 @@ if (
}
const federation = {
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_PORT: process.env.FEDERATION_PORT || 5010,
FEDERATION_API: process.env.FEDERATION_API || '1_0',
FEDERATION_PORT: process.env.FEDERATION_PORT || 5010,
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
}

View File

@ -0,0 +1,13 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class GetPublicKeyResult {
constructor(pubKey: string) {
this.publicKey = pubKey
}
@Field(() => String)
publicKey: string
}

View File

@ -0,0 +1,53 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import { Community as DbCommunity } from '@entity/Community'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
DbCommunity.clear()
})
afterAll(async () => {
await con.close()
})
describe('PublicKeyResolver', () => {
const getPublicKeyQuery = `
query {
getPublicKey
{
publicKey
}
}
`
describe('getPublicKey', () => {
beforeEach(async () => {
const homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.apiVersion = '1_0'
homeCom.endPoint = 'endpoint-url'
homeCom.publicKey = Buffer.from('homeCommunity-publicKey')
await DbCommunity.insert(homeCom)
})
it('returns homeCommunity-publicKey', async () => {
await expect(query({ query: getPublicKeyQuery })).resolves.toMatchObject({
data: {
getPublicKey: {
publicKey: expect.stringMatching('homeCommunity-publicKey'),
},
},
})
})
})
})

View File

@ -0,0 +1,20 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { GetPublicKeyResult } from '../model/GetPublicKeyResult'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class PublicKeyResolver {
@Query(() => GetPublicKeyResult)
async getPublicKey(): Promise<GetPublicKeyResult> {
logger.debug(`getPublicKey() via apiVersion=1_0 ...`)
const homeCom = await DbCommunity.findOneOrFail({
foreign: false,
apiVersion: '1_0',
})
logger.info(`getPublicKey()-1_0... return publicKey=${homeCom.publicKey}`)
return new GetPublicKeyResult(homeCom.publicKey.toString())
}
}

View File

@ -20,7 +20,7 @@ async function main() {
if (CONFIG.GRAPHIQL) {
// eslint-disable-next-line no-console
console.log(
`GraphIQL available at http://localhost:${CONFIG.FEDERATION_PORT}`
`GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`
)
}
})

View File

@ -1,13 +1,18 @@
module.exports = {
verbose: true,
collectCoverage: true,
collectCoverageFrom: ['src/**/*.{js,vue}', '!**/node_modules/**', '!**/?(*.)+(spec|test).js?(x)'],
coverageThreshold: {
global: {
lines: 95,
},
},
moduleFileExtensions: [
'js',
// 'jsx',
'json',
'vue',
],
// coverageReporters: ['lcov', 'text'],
moduleNameMapper: {
'\\.(css|less)$': 'identity-obj-proxy',
'\\.(scss)$': '<rootDir>/src/assets/mocks/styleMock.js',

View File

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

View File

@ -49,15 +49,15 @@ describe('Sidebar', () => {
expect(wrapper.findAll('.nav-item').at(2).text()).toEqual('navigation.transactions')
})
it('has nav-item "gdt.gdt" in navbar', () => {
it('has nav-item "creation" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(3).text()).toEqual('creation')
})
it('has nav-item "creation" in navbar', () => {
it('has nav-item "GDT" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(4).text()).toContain('GDT')
})
it('has nav-item "Information" in navbar', () => {
it('has nav-item "navigation.info" in navbar', () => {
expect(wrapper.findAll('.nav-item').at(5).text()).toContain('navigation.info')
})
})
@ -68,13 +68,13 @@ describe('Sidebar', () => {
expect(wrapper.findAll('ul').at(1).findAll('.nav-item')).toHaveLength(2)
})
it('has nav-item "navigation.info" in navbar', () => {
it('has nav-item "navigation.settings" in navbar', () => {
expect(wrapper.findAll('ul').at(1).findAll('.nav-item').at(0).text()).toEqual(
'navigation.settings',
)
})
it('has nav-item "navigation.settings" in navbar', () => {
it('has nav-item "navigation.logout" in navbar', () => {
expect(wrapper.findAll('ul').at(1).findAll('.nav-item').at(1).text()).toEqual(
'navigation.logout',
)

View File

@ -48,7 +48,12 @@
<b-icon icon="shield-check" aria-hidden="true"></b-icon>
<span class="ml-2">{{ $t('navigation.admin_area') }}</span>
</b-nav-item>
<b-nav-item class="font-weight-bold" @click="$emit('logout')" active-class="activeRoute">
<b-nav-item
class="font-weight-bold"
@click="$emit('logout')"
active-class="activeRoute"
data-test="logout-menu"
>
<b-img src="/img/svg/logout.svg" height="20" class="svg-icon" />
<span class="ml-2 text-205">{{ $t('navigation.logout') }}</span>
</b-nav-item>

View File

@ -7,7 +7,8 @@
"author": "Ulf Gebhardt <ulf.gebhardt@webcraft-media.de>",
"license": "Apache-2.0",
"scripts": {
"release": "scripts/release.sh"
"release": "scripts/release.sh",
"installAll": "yarn && cd database && yarn && cd ../frontend && yarn && cd ../admin && yarn && cd ../backend && yarn && cd ../e2e-tests && yarn && cd ../federation && yarn && cd ../dht-node && yarn && cd .."
},
"dependencies": {
"auto-changelog": "^2.4.0",

View File

@ -7,6 +7,8 @@ FRONTEND_DIR="${PROJECT_DIR}/frontend/"
BACKEND_DIR="${PROJECT_DIR}/backend/"
DATABASE_DIR="${PROJECT_DIR}/database/"
ADMIN_DIR="${PROJECT_DIR}/admin/"
DHTNODE_DIR="${PROJECT_DIR}/dht-node/"
FEDERATION_DIR="${PROJECT_DIR}/federation/"
# navigate to project directory
cd ${PROJECT_DIR}
@ -26,6 +28,10 @@ cd ${DATABASE_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${ADMIN_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${DHTNODE_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
cd ${FEDERATION_DIR}
yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version ${VERSION}
# generate changelog
cd ${PROJECT_DIR}