mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 1170_Send_new_Users_Gradido
This commit is contained in:
commit
3e3b5871a4
98
.github/workflows/publish.yml
vendored
98
.github/workflows/publish.yml
vendored
@ -106,82 +106,6 @@ jobs:
|
||||
name: docker-database-production_up
|
||||
path: /tmp/database_up.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD PRODUCTION LOGIN SERVER ##################################
|
||||
##############################################################################
|
||||
build_production_login_server:
|
||||
name: Docker Build Production - Login Server
|
||||
runs-on: ubuntu-latest
|
||||
#needs: [nothing]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
##########################################################################
|
||||
# SET ENVS ###############################################################
|
||||
##########################################################################
|
||||
- name: ENV - VERSION
|
||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_DATE
|
||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_VERSION
|
||||
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_COMMIT
|
||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||
##########################################################################
|
||||
# LOGIN SERVER ###########################################################
|
||||
##########################################################################
|
||||
- name: Login Server | Build `production` image
|
||||
run: |
|
||||
docker build -t "gradido/login_server:latest" -t "gradido/login_server:production" -t "gradido/login_server:${VERSION}" -t "gradido/login_server:${BUILD_VERSION}" login_server/
|
||||
docker save "gradido/login_server" > /tmp/login_server.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docker-login-server-production
|
||||
path: /tmp/login_server.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD PRODUCTION COMMUNITY SERVER ##############################
|
||||
##############################################################################
|
||||
build_production_community_server:
|
||||
name: Docker Build Production - Community Server
|
||||
runs-on: ubuntu-latest
|
||||
#needs: [nothing]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# SET ENVS ###############################################################
|
||||
##########################################################################
|
||||
- name: ENV - VERSION
|
||||
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_DATE
|
||||
run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_VERSION
|
||||
run: echo "BUILD_VERSION=${VERSION}.${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
|
||||
- name: ENV - BUILD_COMMIT
|
||||
run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
|
||||
##########################################################################
|
||||
# COMMUNITY SERVER #######################################################
|
||||
##########################################################################
|
||||
- name: Community Server | Build `production` image
|
||||
run: |
|
||||
docker build -t "gradido/community_server:latest" -t "gradido/community_server:production" -t "gradido/community_server:${VERSION}" -t "gradido/community_server:${BUILD_VERSION}" -f ./community_server/Dockerfile ./
|
||||
docker save "gradido/community_server" > /tmp/community_server.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docker-community-server-production
|
||||
path: /tmp/community_server.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD PRODUCTION MARIADB #######################################
|
||||
##############################################################################
|
||||
@ -262,7 +186,7 @@ jobs:
|
||||
upload_to_dockerhub:
|
||||
name: Upload to Dockerhub
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_production_frontend, build_production_backend, build_production_database_up, build_production_login_server, build_production_community_server, build_production_mariadb, build_production_nginx]
|
||||
needs: [build_production_frontend, build_production_backend, build_production_database_up, build_production_mariadb, build_production_nginx]
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@ -296,20 +220,6 @@ jobs:
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/database_up.tar
|
||||
- name: Download Docker Image (Login Server)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-login-server-production
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/login_server.tar
|
||||
- name: Download Docker Image (Community Server)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-community-server-production
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/community_server.tar
|
||||
- name: Download Docker Image (MariaDB)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
@ -335,10 +245,6 @@ jobs:
|
||||
run: docker push --all-tags gradido/backend
|
||||
- name: Push database
|
||||
run: docker push --all-tags gradido/database
|
||||
- name: Push login_server
|
||||
run: docker push --all-tags gradido/login_server
|
||||
- name: Push community_server
|
||||
run: docker push --all-tags gradido/community_server
|
||||
- name: Push MariaDB
|
||||
run: docker push --all-tags gradido/mariadb
|
||||
- name: Push Nginx
|
||||
@ -399,7 +305,7 @@ jobs:
|
||||
- name: yarn install
|
||||
run: yarn install
|
||||
- name: generate changelog
|
||||
run: yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
|
||||
run: yarn auto-changelog --commit-limit 0 --latest-version ${{ env.VERSION }} --unreleased-only
|
||||
- name: package-version-to-git-release
|
||||
continue-on-error: true # Will fail if tag exists
|
||||
id: create_release
|
||||
|
||||
215
.github/workflows/test.yml
vendored
215
.github/workflows/test.yml
vendored
@ -107,54 +107,6 @@ jobs:
|
||||
name: docker-database-test_up
|
||||
path: /tmp/database_up.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD TEST LOGIN SERVER ########################################
|
||||
##############################################################################
|
||||
build_test_login_server:
|
||||
name: Docker Build Test - Login Server
|
||||
runs-on: ubuntu-latest
|
||||
#needs: [nothing]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
##########################################################################
|
||||
# BUILD LOGIN SERVER DOCKER IMAGE ########################################
|
||||
##########################################################################
|
||||
- name: login server | Build `release` image
|
||||
run: |
|
||||
docker build -t "gradido/login_server:release" -f ./login_server/Dockerfile login_server/
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD TEST COMMUNITY SERVER ####################################
|
||||
##############################################################################
|
||||
build_test_community_server:
|
||||
name: Docker Build Test - Community Server
|
||||
runs-on: ubuntu-latest
|
||||
#needs: [nothing]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# BUILD COMMUNITY SERVER DOCKER IMAGE ####################################
|
||||
##########################################################################
|
||||
- name: community server | Build `test` image
|
||||
run: |
|
||||
docker build -t "gradido/community_server:test" -f ./community_server/Dockerfile ./
|
||||
docker save "gradido/community_server:test" > /tmp/community_server.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: docker-community-server-test
|
||||
path: /tmp/community_server.tar
|
||||
|
||||
##############################################################################
|
||||
# JOB: DOCKER BUILD TEST MARIADB #############################################
|
||||
##############################################################################
|
||||
@ -470,7 +422,7 @@ jobs:
|
||||
report_name: Coverage Admin Interface
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 77
|
||||
min_coverage: 95
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -479,7 +431,7 @@ jobs:
|
||||
unit_test_backend:
|
||||
name: Unit tests - Backend
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_backend,build_test_mariadb]
|
||||
needs: [build_test_mariadb]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
@ -496,20 +448,21 @@ jobs:
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/mariadb.tar
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/backend.tar
|
||||
##########################################################################
|
||||
# UNIT TESTS BACKEND #####################################################
|
||||
##########################################################################
|
||||
- name: backend | docker-compose
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb database
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
- name: backend | docker-compose database
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||
- name: Sleep for 30 seconds
|
||||
run: sleep 30s
|
||||
shell: bash
|
||||
- name: backend Unit tests | test
|
||||
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn CI_worklfow_test
|
||||
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
|
||||
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
|
||||
##########################################################################
|
||||
# COVERAGE CHECK BACKEND #################################################
|
||||
@ -520,149 +473,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 45
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
# JOB: UNIT TEST LOGIN-SERVER ###############################################
|
||||
##############################################################################
|
||||
unit_test_login_server:
|
||||
name: Unit tests - Login-Server
|
||||
runs-on: ubuntu-latest
|
||||
needs: []
|
||||
services:
|
||||
mariadb:
|
||||
image: gradido/mariadb:test
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_PASSWORD: 1
|
||||
MARIADB_USER: root
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: --health-cmd="mysqladmin ping"
|
||||
--health-interval=6s
|
||||
--health-timeout=3s
|
||||
--health-retries=4
|
||||
steps:
|
||||
# - name: Debug service
|
||||
# run: echo "$(docker ps)"
|
||||
#- name: Debug container choosing script
|
||||
# run: echo "$(docker container ls | grep mariadb | awk '{ print $1 }')"
|
||||
- name: get mariadb container id
|
||||
run: echo "::set-output name=id::$(docker container ls | grep mariadb | awk '{ print $1 }')"
|
||||
id: mariadb_container
|
||||
- name: get automatic created network
|
||||
run: echo "::set-output name=id::$(docker network ls | grep github_network | awk '{ print $1 }')"
|
||||
id: network
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
# Database migration
|
||||
- name: Start database migration
|
||||
run: |
|
||||
docker build --target production_up -t "gradido/database:production_up" database/
|
||||
docker run --network ${{ steps.network.outputs.id }} --name=database --env NODE_ENV=production --env DB_HOST=mariadb --env DB_DATABASE=gradido_community_test -d gradido/database:production_up
|
||||
##########################################################################
|
||||
# Build Login-Server Test Docker image ###################################
|
||||
##########################################################################
|
||||
- name: login server | Build `test` image
|
||||
run: |
|
||||
docker build -t "gradido/login_server:test" -f ./login_server/Dockerfiles/ubuntu/Dockerfile.test login_server/
|
||||
##########################################################################
|
||||
# UNIT TESTS BACKEND LOGIN-SERVER #######################################
|
||||
##########################################################################
|
||||
- name: Login-Server | Unit tests
|
||||
run: |
|
||||
docker run --network container:$(docker container ls | grep mariadb | awk '{ print $1 }') -v ~/coverage:/code/build_cov/coverage -v $(pwd)/configs/login_server:/etc/grd_login gradido/login_server:test
|
||||
cp -r ~/coverage ./coverage
|
||||
##########################################################################
|
||||
# COVERAGE CHECK BACKEND LOGIN-SERVER ####################################
|
||||
##########################################################################
|
||||
- name: backend login | Coverage check
|
||||
uses: webcraftmedia/coverage-check-action@master
|
||||
with:
|
||||
report_name: Coverage Backend Login
|
||||
type: lcov
|
||||
result_path: ./coverage/coverage.info
|
||||
min_coverage: 34
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
# JOB: UNIT TEST COMMUNITY-SERVER ###########################################
|
||||
##############################################################################
|
||||
unit_test_community_server:
|
||||
name: Unit tests - Community Server
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_community_server]
|
||||
services:
|
||||
mariadb:
|
||||
image: gradido/mariadb:test
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_PASSWORD: 1
|
||||
MARIADB_USER: root
|
||||
options: --health-cmd="mysqladmin ping"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=3
|
||||
steps:
|
||||
- name: get mariadb container id
|
||||
run: echo "::set-output name=id::$(docker container ls | grep mariadb | awk '{ print $1 }')"
|
||||
id: mariadb_container
|
||||
- name: get automatic created network
|
||||
run: echo "::set-output name=id::$(docker network ls | grep github_network | awk '{ print $1 }')"
|
||||
id: network
|
||||
- name: Start database migration
|
||||
run: docker run --network ${{ steps.network.outputs.id }} --name=database --env NODE_ENV=production --env DB_HOST=mariadb --env DB_DATABASE=gradido_community_test -d gradido/database:production_up
|
||||
- name: get database migration container id
|
||||
run: echo "::set-output name=id::$(docker container ls | grep database | awk '{ print $1 }')"
|
||||
id: database_container
|
||||
- name: Start Login-Server
|
||||
run: docker run --network ${{ steps.network.outputs.id }} --name=login-server -d gradido/login_server:with-config
|
||||
- name: get login-server container id
|
||||
run: echo "::set-output name=id::$(docker container ls | grep login_server | awk '{ print $1 }')"
|
||||
id: login_server_container
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
##########################################################################
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
##########################################################################
|
||||
# DOWNLOAD DOCKER IMAGE ##################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Community-Server)
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: docker-community-server-test
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/community_server.tar
|
||||
# for debugging login-server
|
||||
- name: check login-server
|
||||
run: docker logs ${{ steps.login_server_container.outputs.id }}
|
||||
- name: check mariadb
|
||||
run: docker logs ${{ steps.mariadb_container.outputs.id }}
|
||||
- name: check migration
|
||||
run: docker logs ${{ steps.database_container.outputs.id }}
|
||||
##########################################################################
|
||||
# UNIT TESTS BACKEND COMMUNITY-SERVER #######################################
|
||||
##########################################################################
|
||||
- name: community server | Unit tests
|
||||
run: |
|
||||
docker run --network ${{ steps.network.outputs.id }} -v ~/coverage:/var/www/cakephp/webroot/coverage gradido/community_server:test
|
||||
cp -r ~/coverage ./coverage
|
||||
#########################################################################
|
||||
# COVERAGE CHECK BACKEND COMMUNITY-SERVER ####################################
|
||||
##########################################################################
|
||||
- name: backend community | Coverage check
|
||||
uses: einhornimmond/coverage-check-action@master
|
||||
with:
|
||||
report_name: Coverage Backend Community
|
||||
type: phpunit
|
||||
result_path: ./coverage/coverage.info
|
||||
min_coverage: 10
|
||||
min_coverage: 48
|
||||
token: ${{ github.token }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
35
.gitmodules
vendored
35
.gitmodules
vendored
@ -1,36 +1 @@
|
||||
[submodule "gn"]
|
||||
path = gn
|
||||
url = https://github.com/gradido/gn.git
|
||||
branch = master
|
||||
|
||||
[submodule "login_server/dependencies/tinf"]
|
||||
path = login_server/dependencies/tinf
|
||||
url = https://github.com/jibsen/tinf.git
|
||||
[submodule "login_server/dependencies/mariadb-connector-c"]
|
||||
path = login_server/dependencies/mariadb-connector-c
|
||||
url = https://github.com/MariaDB/mariadb-connector-c.git
|
||||
[submodule "login_server/src/proto"]
|
||||
path = login_server/src/proto
|
||||
url = https://github.com/gradido/gradido_protocol.git
|
||||
[submodule "login_server/dependencies/spirit-po"]
|
||||
path = login_server/dependencies/spirit-po
|
||||
url = https://github.com/cbeck88/spirit-po.git
|
||||
[submodule "login_server/dependencies/poco"]
|
||||
path = login_server/dependencies/poco
|
||||
url = https://github.com/pocoproject/poco.git
|
||||
[submodule "login_server/dependencies/cmake-modules"]
|
||||
path = login_server/dependencies/cmake-modules
|
||||
url = https://github.com/viaduck/cmake-modules.git
|
||||
|
||||
[submodule "community_server/src/protobuf"]
|
||||
path = community_server/src/protobuf
|
||||
url = https://github.com/gradido/gradido_protocol.git
|
||||
[submodule "login_server/dependencies/libsodium"]
|
||||
path = login_server/dependencies/libsodium
|
||||
url = https://github.com/jedisct1/libsodium.git
|
||||
[submodule "login_server/src/proto"]
|
||||
path = login_server/src/proto
|
||||
url = https://github.com/gradido/gradido_protocol.git
|
||||
[submodule "login_server/dependencies/protobuf"]
|
||||
path = login_server/dependencies/protobuf
|
||||
url = https://github.com/protocolbuffers/protobuf.git
|
||||
|
||||
308
CHANGELOG.md
308
CHANGELOG.md
@ -4,12 +4,274 @@ All notable changes to this project will be documented in this file. Dates are d
|
||||
|
||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
#### [1.6.6](https://github.com/gradido/gradido/compare/1.6.5...1.6.6)
|
||||
|
||||
- Fix: Upper case email on register breaks account [`#1542`](https://github.com/gradido/gradido/pull/1542)
|
||||
- 1106 first transaction cannot be expanded [`#1432`](https://github.com/gradido/gradido/pull/1432)
|
||||
- added missing bootstrap scss. bootstrap/scss/bootstrap, plus more mis… [`#1540`](https://github.com/gradido/gradido/pull/1540)
|
||||
- feat: Seed Deleted User [`#1533`](https://github.com/gradido/gradido/pull/1533)
|
||||
- fix: No Creations for Deleted Users [`#1534`](https://github.com/gradido/gradido/pull/1534)
|
||||
- fix: Wrong Key Name for Recover User [`#1535`](https://github.com/gradido/gradido/pull/1535)
|
||||
- [Feature] : user deleted and undeleted functions for adminarea [`#1520`](https://github.com/gradido/gradido/pull/1520)
|
||||
- fix: Possible SQL Exception in User Search [`#1530`](https://github.com/gradido/gradido/pull/1530)
|
||||
- Feature: Make lint warnings unwanted [`#1529`](https://github.com/gradido/gradido/pull/1529)
|
||||
- 1459 list data again on confirm creation [`#1467`](https://github.com/gradido/gradido/pull/1467)
|
||||
- fix: Return Empty Array When No Pending Creations Are Present [`#1526`](https://github.com/gradido/gradido/pull/1526)
|
||||
- Fix: Correct path of index.js in production [`#1525`](https://github.com/gradido/gradido/pull/1525)
|
||||
- refactor: Get Open Creations by One Query [`#1524`](https://github.com/gradido/gradido/pull/1524)
|
||||
- Admin: Langsame Benutzer-Suche [`#1472`](https://github.com/gradido/gradido/pull/1472)
|
||||
- fix: Backend Unit Tests Running Again [`#1513`](https://github.com/gradido/gradido/pull/1513)
|
||||
- Refactor: Combine transaction tables [`#1523`](https://github.com/gradido/gradido/pull/1523)
|
||||
- Refactor: User resolver [`#1522`](https://github.com/gradido/gradido/pull/1522)
|
||||
- feature: Soft-Delete for users (backend) [`#1521`](https://github.com/gradido/gradido/pull/1521)
|
||||
- feature: Soft-Delete for users (database only) [`#1516`](https://github.com/gradido/gradido/pull/1516)
|
||||
- refactor: Improve Decay Display [`#1517`](https://github.com/gradido/gradido/pull/1517)
|
||||
- 404 page needs back to login button [`#1515`](https://github.com/gradido/gradido/pull/1515)
|
||||
- feature: show current version in admin footer [`#1514`](https://github.com/gradido/gradido/pull/1514)
|
||||
- fix: Never Sent Email Text [`#1512`](https://github.com/gradido/gradido/pull/1512)
|
||||
- refactor: static decay block [`#1405`](https://github.com/gradido/gradido/pull/1405)
|
||||
- refactor: Use Bootstrap Vue Toast [`#1499`](https://github.com/gradido/gradido/pull/1499)
|
||||
- fix: Catch GDT Server Errors [`#1479`](https://github.com/gradido/gradido/pull/1479)
|
||||
- Fix: Autochangelog - no commits [`#1498`](https://github.com/gradido/gradido/pull/1498)
|
||||
|
||||
#### [1.6.5](https://github.com/gradido/gradido/compare/1.6.4...1.6.5)
|
||||
|
||||
> 15 February 2022
|
||||
|
||||
- v1.6.5 [`#1497`](https://github.com/gradido/gradido/pull/1497)
|
||||
- Fix: Elopage Hook Crash 2 [`#1481`](https://github.com/gradido/gradido/pull/1481)
|
||||
|
||||
#### [1.6.4](https://github.com/gradido/gradido/compare/1.6.3...1.6.4)
|
||||
|
||||
> 14 February 2022
|
||||
|
||||
- v1.6.4 [`#1478`](https://github.com/gradido/gradido/pull/1478)
|
||||
- fix: Admin Email Confirmation Date and Time [`#1448`](https://github.com/gradido/gradido/pull/1448)
|
||||
- Fix: Do not log password or token to the console [`#1477`](https://github.com/gradido/gradido/pull/1477)
|
||||
- Fix: Elopage Hook Crash [`#1474`](https://github.com/gradido/gradido/pull/1474)
|
||||
- 538 unify all buttons [`#1455`](https://github.com/gradido/gradido/pull/1455)
|
||||
- 833 old error is shown for a second even if transaction is successful [`#1460`](https://github.com/gradido/gradido/pull/1460)
|
||||
- fix: Wrong Email Spelling in German [`#1446`](https://github.com/gradido/gradido/pull/1446)
|
||||
- fix: Redirect to Login after Register [`#1445`](https://github.com/gradido/gradido/pull/1445)
|
||||
- refactor: Split User Table Component in Admin Interface [`#1443`](https://github.com/gradido/gradido/pull/1443)
|
||||
|
||||
#### [1.6.3](https://github.com/gradido/gradido/compare/1.6.2...1.6.3)
|
||||
|
||||
> 9 February 2022
|
||||
|
||||
- v1.6.3 [`#1447`](https://github.com/gradido/gradido/pull/1447)
|
||||
- add .btn-outline-secondary in scss [`#1442`](https://github.com/gradido/gradido/pull/1442)
|
||||
- Profil settings and footer refactor [`#1440`](https://github.com/gradido/gradido/pull/1440)
|
||||
|
||||
#### [1.6.2](https://github.com/gradido/gradido/compare/1.6.1...1.6.2)
|
||||
|
||||
> 8 February 2022
|
||||
|
||||
- v1.6.2 [`#1438`](https://github.com/gradido/gradido/pull/1438)
|
||||
- updated_changelog_library [`#1437`](https://github.com/gradido/gradido/pull/1437)
|
||||
- admin interface does user have member area [`#1416`](https://github.com/gradido/gradido/pull/1416)
|
||||
- Refactor - Remove community_server [`#1408`](https://github.com/gradido/gradido/pull/1408)
|
||||
- 1389 transactions tabs are not well designed [`#1425`](https://github.com/gradido/gradido/pull/1425)
|
||||
- fix_community_name_description [`#1429`](https://github.com/gradido/gradido/pull/1429)
|
||||
- remove_unnecessary_repositories [`#1406`](https://github.com/gradido/gradido/pull/1406)
|
||||
- clean_database_users [`#1427`](https://github.com/gradido/gradido/pull/1427)
|
||||
- remove_gradido_node [`#1431`](https://github.com/gradido/gradido/pull/1431)
|
||||
- add updateTransactions function for GDD balance if reload page [`#1423`](https://github.com/gradido/gradido/pull/1423)
|
||||
- 1390 display error when navigating to send form without any gdd [`#1424`](https://github.com/gradido/gradido/pull/1424)
|
||||
- have an delete button for the search input [`#1413`](https://github.com/gradido/gradido/pull/1413)
|
||||
- reset all selected users in mass creation [`#1422`](https://github.com/gradido/gradido/pull/1422)
|
||||
- combine_user_tables [`#1411`](https://github.com/gradido/gradido/pull/1411)
|
||||
- feat: Test Table Row Details Toggling [`#1420`](https://github.com/gradido/gradido/pull/1420)
|
||||
- feat: Improved Tests for Mass Creation [`#1419`](https://github.com/gradido/gradido/pull/1419)
|
||||
- refactor: Mixin for Creation Labels [`#1409`](https://github.com/gradido/gradido/pull/1409)
|
||||
- Marque community_server as to be removed. [`#1407`](https://github.com/gradido/gradido/pull/1407)
|
||||
- database_transaction_signatures [`#1368`](https://github.com/gradido/gradido/pull/1368)
|
||||
- database_pending_creations [`#1367`](https://github.com/gradido/gradido/pull/1367)
|
||||
- fix_seed [`#1410`](https://github.com/gradido/gradido/pull/1410)
|
||||
- clean_database [`#1362`](https://github.com/gradido/gradido/pull/1362)
|
||||
- multiple creation already selected users remain saved [`#1376`](https://github.com/gradido/gradido/pull/1376)
|
||||
- fix: Localize Datetime in Admin Interface [`#1327`](https://github.com/gradido/gradido/pull/1327)
|
||||
- feat: Remove Login Server [`#1383`](https://github.com/gradido/gradido/pull/1383)
|
||||
- refactor: Tag Last Version with Login Server [`#1391`](https://github.com/gradido/gradido/pull/1391)
|
||||
- if an email is not confirmed, a user cannot be added to any multiple … [`#1374`](https://github.com/gradido/gradido/pull/1374)
|
||||
- cleanups_refactors [`#1404`](https://github.com/gradido/gradido/pull/1404)
|
||||
- 1365 clear bootstrap version for vue2, preparation for new template [`#1366`](https://github.com/gradido/gradido/pull/1366)
|
||||
- upgrade vue version from ^2.6.11 to 2.6.12 [`#1382`](https://github.com/gradido/gradido/pull/1382)
|
||||
- remove vue-qrcode from dashboard-plugin [`#1364`](https://github.com/gradido/gradido/pull/1364)
|
||||
- remove unused package from frontend [`#1360`](https://github.com/gradido/gradido/pull/1360)
|
||||
|
||||
#### [1.6.1](https://github.com/gradido/gradido/compare/1.6.0...1.6.1)
|
||||
|
||||
> 28 January 2022
|
||||
|
||||
- Hotfix elopage [`#1358`](https://github.com/gradido/gradido/pull/1358)
|
||||
- change standard text für creation [`#1343`](https://github.com/gradido/gradido/pull/1343)
|
||||
- Check if user email is activated to make a creation. [`#1356`](https://github.com/gradido/gradido/pull/1356)
|
||||
- fix: Creation Confirmation User Ids [`#1345`](https://github.com/gradido/gradido/pull/1345)
|
||||
|
||||
#### [1.6.0](https://github.com/gradido/gradido/compare/1.5.1...1.6.0)
|
||||
|
||||
> 27 January 2022
|
||||
|
||||
- v1.6.0 [`#1357`](https://github.com/gradido/gradido/pull/1357)
|
||||
- fix updatePendingCreation method [`#1346`](https://github.com/gradido/gradido/pull/1346)
|
||||
- klicktipp_config [`#1348`](https://github.com/gradido/gradido/pull/1348)
|
||||
- 1351 form to create incorrect due to change of bootstrap version [`#1352`](https://github.com/gradido/gradido/pull/1352)
|
||||
- first draft of releaseplan [`#1349`](https://github.com/gradido/gradido/pull/1349)
|
||||
- Rechtschreibfehler behoben, entgültig zu endgültig [`#1354`](https://github.com/gradido/gradido/pull/1354)
|
||||
- production_deployment [`#1341`](https://github.com/gradido/gradido/pull/1341)
|
||||
- import_production_data [`#1324`](https://github.com/gradido/gradido/pull/1324)
|
||||
- remove division / 100 for GDT [`#1339`](https://github.com/gradido/gradido/pull/1339)
|
||||
- fix: Language Set Correctly after Page Reload in Admin Interface [`#1334`](https://github.com/gradido/gradido/pull/1334)
|
||||
- fix: Set Locale after Login [`#1328`](https://github.com/gradido/gradido/pull/1328)
|
||||
- deployment_community_parameters [`#1331`](https://github.com/gradido/gradido/pull/1331)
|
||||
- bold text if acive page [`#1337`](https://github.com/gradido/gradido/pull/1337)
|
||||
- 1303 thanks page after set password [`#1335`](https://github.com/gradido/gradido/pull/1335)
|
||||
- Changed the toasted message for successful creating a creation. [`#1332`](https://github.com/gradido/gradido/pull/1332)
|
||||
- Added a required to the password repeat field, errors are shown immed… [`#1333`](https://github.com/gradido/gradido/pull/1333)
|
||||
- fix_rejected_login [`#1329`](https://github.com/gradido/gradido/pull/1329)
|
||||
- defined fields for Transactionlist in UserSearch in Adminarea, add Lo… [`#1322`](https://github.com/gradido/gradido/pull/1322)
|
||||
- fix: Render Localized Balance in NavBar [`#1321`](https://github.com/gradido/gradido/pull/1321)
|
||||
- 1227 vergaenglichkeit besser darstellen [`#1312`](https://github.com/gradido/gradido/pull/1312)
|
||||
- 1300 gray out register button further when disabled [`#1316`](https://github.com/gradido/gradido/pull/1316)
|
||||
- change Admin Gradido-Logo font color to white [`#1315`](https://github.com/gradido/gradido/pull/1315)
|
||||
- 1299 remove text from register publisher [`#1317`](https://github.com/gradido/gradido/pull/1317)
|
||||
- 1249 gdd gdt values look displaced or cut off [`#1318`](https://github.com/gradido/gradido/pull/1318)
|
||||
- 1307 admin rename wallet to my account [`#1314`](https://github.com/gradido/gradido/pull/1314)
|
||||
- 1011 visualize and refine the register process [`#1136`](https://github.com/gradido/gradido/pull/1136)
|
||||
- New roadmap [`#1219`](https://github.com/gradido/gradido/pull/1219)
|
||||
- fix: Redirect after Email Confirmation [`#1308`](https://github.com/gradido/gradido/pull/1308)
|
||||
- fix_docker_logging [`#1309`](https://github.com/gradido/gradido/pull/1309)
|
||||
- feat: Paginate User Table [`#1293`](https://github.com/gradido/gradido/pull/1293)
|
||||
- Adding button, to link back to the login page from the error not activated page. [`#1297`](https://github.com/gradido/gradido/pull/1297)
|
||||
- [WIP] new_deployment_stage2 [`#1290`](https://github.com/gradido/gradido/pull/1290)
|
||||
- add icons in menu and navbarmenu [`#1298`](https://github.com/gradido/gradido/pull/1298)
|
||||
- Withdrew horrizontal scrollbar [`#1295`](https://github.com/gradido/gradido/pull/1295)
|
||||
- transactionCount false or transactionCount error text hide [`#1259`](https://github.com/gradido/gradido/pull/1259)
|
||||
- 1265 mouse hovering over pencil in settings [`#1270`](https://github.com/gradido/gradido/pull/1270)
|
||||
- Fix redirect to /logout, now redirects to the wallet login. [`#1285`](https://github.com/gradido/gradido/pull/1285)
|
||||
- feat: Test and Refactor Send Email [`#1224`](https://github.com/gradido/gradido/pull/1224)
|
||||
- new_deployment [`#1220`](https://github.com/gradido/gradido/pull/1220)
|
||||
- Withdrew space at the end of an locale definition. [`#1283`](https://github.com/gradido/gradido/pull/1283)
|
||||
- Fix removing from masscreationlist now gives right item to the left l… [`#1284`](https://github.com/gradido/gradido/pull/1284)
|
||||
- onboarding process document [`#1148`](https://github.com/gradido/gradido/pull/1148)
|
||||
- Don't show the language-switch on the /register page. [`#1282`](https://github.com/gradido/gradido/pull/1282)
|
||||
- language_updates_bernd [`#1260`](https://github.com/gradido/gradido/pull/1260)
|
||||
- Admin Logout redirectes to the wallet login page. [`#1271`](https://github.com/gradido/gradido/pull/1271)
|
||||
- fix: Admin Creation List [`#1276`](https://github.com/gradido/gradido/pull/1276)
|
||||
- fix: Override Existing Token in Response Header [`#1278`](https://github.com/gradido/gradido/pull/1278)
|
||||
- fix: Pass Language to Admin Interface [`#1280`](https://github.com/gradido/gradido/pull/1280)
|
||||
- feat: Seed Garrick Ollivander [`#1277`](https://github.com/gradido/gradido/pull/1277)
|
||||
- fix: Fix Update of Open Creations [`#1273`](https://github.com/gradido/gradido/pull/1273)
|
||||
- fix: Change Password in User Profile [`#1266`](https://github.com/gradido/gradido/pull/1266)
|
||||
- Wallet shows the wrong gdt sum (gdtSum * 100) [`#1269`](https://github.com/gradido/gradido/pull/1269)
|
||||
- 1240 account not activated needs a button to login page [`#1261`](https://github.com/gradido/gradido/pull/1261)
|
||||
- reverse filter for unregistered emails [`#1256`](https://github.com/gradido/gradido/pull/1256)
|
||||
- close row-details automatically when switching to multiple creation page [`#1245`](https://github.com/gradido/gradido/pull/1245)
|
||||
- change creationdatas if change pending creation [`#1244`](https://github.com/gradido/gradido/pull/1244)
|
||||
- 1230 details für wiederholte email bestaetigen versenden [`#1255`](https://github.com/gradido/gradido/pull/1255)
|
||||
- 1221 month names of the open creation in multiple creation [`#1222`](https://github.com/gradido/gradido/pull/1222)
|
||||
- Documentation Template-Overview-2021 PDF [`#1194`](https://github.com/gradido/gradido/pull/1194)
|
||||
- Admin create multiple pending creations [`#1203`](https://github.com/gradido/gradido/pull/1203)
|
||||
- feat: Catch No Cookies Allowed [`#1187`](https://github.com/gradido/gradido/pull/1187)
|
||||
- Remove confirmation of own pending creation [`#1215`](https://github.com/gradido/gradido/pull/1215)
|
||||
- feat: Test Create User Mutation [`#1217`](https://github.com/gradido/gradido/pull/1217)
|
||||
- #751 Änderungen auf Federation und Community-Erstellprozess [`#969`](https://github.com/gradido/gradido/pull/969)
|
||||
- 1055 concept of operations requirements [`#1129`](https://github.com/gradido/gradido/pull/1129)
|
||||
- Admin Translations German English [`#1218`](https://github.com/gradido/gradido/pull/1218)
|
||||
- Roadmap [`#1213`](https://github.com/gradido/gradido/pull/1213)
|
||||
- pr_admin_refactor [`#1214`](https://github.com/gradido/gradido/pull/1214)
|
||||
- 1197 admin interface created transactions list [`#1202`](https://github.com/gradido/gradido/pull/1202)
|
||||
- migration_0006_login_users_collation [`#1207`](https://github.com/gradido/gradido/pull/1207)
|
||||
- wrong_login_is_not_password_reset [`#1179`](https://github.com/gradido/gradido/pull/1179)
|
||||
- Moved reverse of the getPendingCreations to the AdminResolver instead… [`#1185`](https://github.com/gradido/gradido/pull/1185)
|
||||
- Withdrew password argument of createUser. [`#1206`](https://github.com/gradido/gradido/pull/1206)
|
||||
- fix_optin_code_regeneration [`#1195`](https://github.com/gradido/gradido/pull/1195)
|
||||
- 1057 display gdd balance when sending [`#1149`](https://github.com/gradido/gradido/pull/1149)
|
||||
- 1145 refactor admin resolver from master [`#1164`](https://github.com/gradido/gradido/pull/1164)
|
||||
- Error handling in GddTransactionList.vue [`#1183`](https://github.com/gradido/gradido/pull/1183)
|
||||
- state-balance balanceDate can't get the date of the last transaction [`#1182`](https://github.com/gradido/gradido/pull/1182)
|
||||
- feat: Frontend Refactor and Tests [`#1186`](https://github.com/gradido/gradido/pull/1186)
|
||||
- fix: No Nodemon in Database Scripts [`#1167`](https://github.com/gradido/gradido/pull/1167)
|
||||
- feat: Test Creation in Admin Interface [`#1172`](https://github.com/gradido/gradido/pull/1172)
|
||||
- 533 refactor menu remove tim [`#1162`](https://github.com/gradido/gradido/pull/1162)
|
||||
- database_docker_fixes [`#1176`](https://github.com/gradido/gradido/pull/1176)
|
||||
- naming_conventions_pending_tasks [`#1184`](https://github.com/gradido/gradido/pull/1184)
|
||||
- fix_i18n [`#1180`](https://github.com/gradido/gradido/pull/1180)
|
||||
- fix_corrupted_sender_balance [`#1178`](https://github.com/gradido/gradido/pull/1178)
|
||||
- Withdrew * 10000 and / 10000 since we need to store the full value. [`#1181`](https://github.com/gradido/gradido/pull/1181)
|
||||
- catch error Client certificate revoked and pass error.session-expired… [`#1152`](https://github.com/gradido/gradido/pull/1152)
|
||||
- feat: Test Apollo Provider in Frontend [`#1161`](https://github.com/gradido/gradido/pull/1161)
|
||||
- fix: Pending Creations are Updated Without Page Reload [`#1160`](https://github.com/gradido/gradido/pull/1160)
|
||||
- Error on upgrade database after downgrade [`#1119`](https://github.com/gradido/gradido/pull/1119)
|
||||
- Login fix creation validation [`#1159`](https://github.com/gradido/gradido/pull/1159)
|
||||
- login_call_resetPassword [`#1130`](https://github.com/gradido/gradido/pull/1130)
|
||||
- register add tabindex-1 on password show [`#1158`](https://github.com/gradido/gradido/pull/1158)
|
||||
- feat: Seed Transaction Creations [`#1146`](https://github.com/gradido/gradido/pull/1146)
|
||||
- Admin confirm pending creation [`#1153`](https://github.com/gradido/gradido/pull/1153)
|
||||
- Admin delete pending creation query [`#1141`](https://github.com/gradido/gradido/pull/1141)
|
||||
- fix: Catch Expired Session when Coming from Admin Interface [`#1151`](https://github.com/gradido/gradido/pull/1151)
|
||||
- docu_release [`#1138`](https://github.com/gradido/gradido/pull/1138)
|
||||
- feat: Verify Login in Admin Interface [`#1150`](https://github.com/gradido/gradido/pull/1150)
|
||||
- Admin pending creation queries [`#1140`](https://github.com/gradido/gradido/pull/1140)
|
||||
- 1137 publisher id as input field on register [`#1147`](https://github.com/gradido/gradido/pull/1147)
|
||||
- Admin pending creation [`#1135`](https://github.com/gradido/gradido/pull/1135)
|
||||
- feat: Setup Data Seeding [`#1121`](https://github.com/gradido/gradido/pull/1121)
|
||||
- fix_admin_token_renewal [`#1139`](https://github.com/gradido/gradido/pull/1139)
|
||||
- backend_rights [`#1126`](https://github.com/gradido/gradido/pull/1126)
|
||||
- Login admin interface [`#1125`](https://github.com/gradido/gradido/pull/1125)
|
||||
- Adminbereich first step [`#1116`](https://github.com/gradido/gradido/pull/1116)
|
||||
- Login hook elopage [`#1112`](https://github.com/gradido/gradido/pull/1112)
|
||||
- Since we don't make a request to the login_server we need to catch if… [`#1131`](https://github.com/gradido/gradido/pull/1131)
|
||||
- feat: Add Server Users Entity [`#1127`](https://github.com/gradido/gradido/pull/1127)
|
||||
- Stale: 1002-language-selection-register [`#1113`](https://github.com/gradido/gradido/pull/1113)
|
||||
- Error for removing coin animation [`#1120`](https://github.com/gradido/gradido/pull/1120)
|
||||
- Adding a check that the user is activated before letting them login. [`#1099`](https://github.com/gradido/gradido/pull/1099)
|
||||
- Changed the Auto increment so that it is done after the rollback and … [`#1128`](https://github.com/gradido/gradido/pull/1128)
|
||||
- Login call unsecure login [`#1095`](https://github.com/gradido/gradido/pull/1095)
|
||||
- feat: Setup Admin Interface [`#1045`](https://github.com/gradido/gradido/pull/1045)
|
||||
- login_call_updateUserInfos [`#1084`](https://github.com/gradido/gradido/pull/1084)
|
||||
- fix: Await Resolved Promises in Backend Unit Tests [`#1079`](https://github.com/gradido/gradido/pull/1079)
|
||||
- feat: Raise Test Coverage Frontend [`#1102`](https://github.com/gradido/gradido/pull/1102)
|
||||
- login_call_logout [`#1096`](https://github.com/gradido/gradido/pull/1096)
|
||||
- login_call_hasElopage [`#1083`](https://github.com/gradido/gradido/pull/1083)
|
||||
- login_call_create_user [`#1070`](https://github.com/gradido/gradido/pull/1070)
|
||||
- 1036 register page breaks without community [`#1043`](https://github.com/gradido/gradido/pull/1043)
|
||||
- Apollo create transactions without signation [`#1044`](https://github.com/gradido/gradido/pull/1044)
|
||||
- change getCustomRepository to createUser function [`#1046`](https://github.com/gradido/gradido/pull/1046)
|
||||
- fresh_install_instructions [`#1065`](https://github.com/gradido/gradido/pull/1065)
|
||||
- login_call_check_username [`#1037`](https://github.com/gradido/gradido/pull/1037)
|
||||
- feat: Setup Unit Tests for Resolvers in Backend [`#951`](https://github.com/gradido/gradido/pull/951)
|
||||
- Login hotfixes 1.5.1 [`#1075`](https://github.com/gradido/gradido/pull/1075)
|
||||
- vscode_suggest_extensions [`#1073`](https://github.com/gradido/gradido/pull/1073)
|
||||
- Documentation on the coin creation and the steps to have the accounts… [`#1052`](https://github.com/gradido/gradido/pull/1052)
|
||||
- fix open community server user account creation [`#1072`](https://github.com/gradido/gradido/pull/1072)
|
||||
- when creating the register page, everything in the form is set to blank [`#1025`](https://github.com/gradido/gradido/pull/1025)
|
||||
- Spelling error fixed 'Berechnungsformel' [`#1048`](https://github.com/gradido/gradido/pull/1048)
|
||||
- checkEmail.vue page text is displayed correctly now [`#1051`](https://github.com/gradido/gradido/pull/1051)
|
||||
- Adminarea old [`#1058`](https://github.com/gradido/gradido/pull/1058)
|
||||
- migrate_login_database [`#1031`](https://github.com/gradido/gradido/pull/1031)
|
||||
- Text changes for german and english. [`#1041`](https://github.com/gradido/gradido/pull/1041)
|
||||
- refactor: Transaction Component [`#1026`](https://github.com/gradido/gradido/pull/1026)
|
||||
- 1017 fixe dashboard layout safari [`#1038`](https://github.com/gradido/gradido/pull/1038)
|
||||
- 707 separate account overview and send [`#970`](https://github.com/gradido/gradido/pull/970)
|
||||
- Link to the funding contributions of gradido.net memberships [`#984`](https://github.com/gradido/gradido/pull/984)
|
||||
- fix: Decay Rounded to Ceil [`#1021`](https://github.com/gradido/gradido/pull/1021)
|
||||
- fix: Flaky Dashboard Layout Test on Logout [`#1024`](https://github.com/gradido/gradido/pull/1024)
|
||||
- Move Entity models into database [`#956`](https://github.com/gradido/gradido/pull/956)
|
||||
- Text 'No decay' replaced by the number 0.00 [`#1023`](https://github.com/gradido/gradido/pull/1023)
|
||||
- change text 'eingetraten' to 'eingetragen' [`#1022`](https://github.com/gradido/gradido/pull/1022)
|
||||
- fix: Test Default Publisher ID in Sidebar Menu [`#987`](https://github.com/gradido/gradido/pull/987)
|
||||
- increase_frontend_coverage [`#1020`](https://github.com/gradido/gradido/pull/1020)
|
||||
- analyse_bundle [`#1019`](https://github.com/gradido/gradido/pull/1019)
|
||||
- release_issue_template [`#1013`](https://github.com/gradido/gradido/pull/1013)
|
||||
- fix_changelog [`#1014`](https://github.com/gradido/gradido/pull/1014)
|
||||
|
||||
#### [1.5.1](https://github.com/gradido/gradido/compare/1.5.0...1.5.1)
|
||||
|
||||
> 15 October 2021
|
||||
|
||||
- fix isExitInDb [`#994`](https://github.com/gradido/gradido/pull/994)
|
||||
- fix [`80228ef`](https://github.com/gradido/gradido/commit/80228ef842d4087ea4b80934b15b8112611e3e33)
|
||||
|
||||
#### [1.5.0](https://github.com/gradido/gradido/compare/1.4.0...1.5.0)
|
||||
|
||||
@ -86,9 +348,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- update docker files [`#830`](https://github.com/gradido/gradido/pull/830)
|
||||
- added publisher_id field to user [`#245`](https://github.com/gradido/gradido/pull/245)
|
||||
- webpack update [`#811`](https://github.com/gradido/gradido/pull/811)
|
||||
- resolvers [`562ad9a`](https://github.com/gradido/gradido/commit/562ad9ae31d97f90a371452bed1ffe10ebf2d3a5)
|
||||
- deleted inputs (now args) [`8ab542a`](https://github.com/gradido/gradido/commit/8ab542a28acf6b78d7a9e7fe7757363d225f7b4f)
|
||||
- fix UserCard_CoinAnimation to properly use the store, have 100% coverage and other minor fixes & simplifications [`ce826de`](https://github.com/gradido/gradido/commit/ce826deb1d6d92caba514713539dca2da3f74de7)
|
||||
|
||||
#### [1.4.0](https://github.com/gradido/gradido/compare/1.3.1...1.4.0)
|
||||
|
||||
@ -130,9 +389,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- feat: Vue Apollo Client [`#701`](https://github.com/gradido/gradido/pull/701)
|
||||
- change text from Geld to Gradidos [`#711`](https://github.com/gradido/gradido/pull/711)
|
||||
- fix fix [`#728`](https://github.com/gradido/gradido/pull/728)
|
||||
- sort locales [`ec12a28`](https://github.com/gradido/gradido/commit/ec12a28f81577d530f58b42b7f8c2c7d20dffd64)
|
||||
- feat: Unify and Sort Locales [`aba4f4d`](https://github.com/gradido/gradido/commit/aba4f4d20e0a13016e3528a1c5c30c111eb3a9f1)
|
||||
- feat: Increase Coverage [`3c061bc`](https://github.com/gradido/gradido/commit/3c061bcb8d1a3a47442ed6a351e1428e15b314aa)
|
||||
|
||||
#### [1.3.1](https://github.com/gradido/gradido/compare/1.3.0...1.3.1)
|
||||
|
||||
@ -141,9 +397,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- fix: Translations and Formula Display [`#727`](https://github.com/gradido/gradido/pull/727)
|
||||
- 612 docu structure [`#688`](https://github.com/gradido/gradido/pull/688)
|
||||
- Community update for gdt list GDT transaction format [`#726`](https://github.com/gradido/gradido/pull/726)
|
||||
- [#612] new directory structure in /docu/Concepts [`10bf3b0`](https://github.com/gradido/gradido/commit/10bf3b0cdfa6c44f879be0155e93f636601a051b)
|
||||
- #612 additional documents [`ac0ed4f`](https://github.com/gradido/gradido/commit/ac0ed4fee81caff26d09b5de47dd130f12abdb45)
|
||||
- #612 docu restructuring [`e67e1c4`](https://github.com/gradido/gradido/commit/e67e1c41e78264698e6fae4cf1d29751de7e7b29)
|
||||
|
||||
#### [1.3.0](https://github.com/gradido/gradido/compare/1.2.1...1.3.0)
|
||||
|
||||
@ -165,9 +418,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Backend Setup [`#584`](https://github.com/gradido/gradido/pull/584)
|
||||
- text-size in textarea and font-variante if focus [`#677`](https://github.com/gradido/gradido/pull/677)
|
||||
- 680 app large maximum width [`#681`](https://github.com/gradido/gradido/pull/681)
|
||||
- linting, server is working [`34b30b2`](https://github.com/gradido/gradido/commit/34b30b216b6fafcb5b686d4b023b05f2e9766bdf)
|
||||
- server stack seems to work. Graphql does not load properly yet [`43f7cf8`](https://github.com/gradido/gradido/commit/43f7cf87679713d436a64d569d6af1594a12ee33)
|
||||
- initial commit, base packages [`fdf0979`](https://github.com/gradido/gradido/commit/fdf0979830fece04208a6b3bb06bb5323a3c149b)
|
||||
|
||||
#### [1.2.1](https://github.com/gradido/gradido/compare/1.2.0...1.2.1)
|
||||
|
||||
@ -178,9 +428,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- fix unneccessary migration run on fresh (docker) setup [`#654`](https://github.com/gradido/gradido/pull/654)
|
||||
- move back decay as standalone transaction in old frontend [`#656`](https://github.com/gradido/gradido/pull/656)
|
||||
- fix display error with creation [`#652`](https://github.com/gradido/gradido/pull/652)
|
||||
- release [`a0b8056`](https://github.com/gradido/gradido/commit/a0b8056c17b22570a1b1dbb6fa6ce71e561b04af)
|
||||
- update content for frontend [`d37ce09`](https://github.com/gradido/gradido/commit/d37ce0949ef97d2a6c6ffaf0be31db9f6d92e743)
|
||||
- exchange positions [`bc000ef`](https://github.com/gradido/gradido/commit/bc000efd87c9701480c4aeaa7b819ab49bfe8f01)
|
||||
|
||||
#### [1.2.0](https://github.com/gradido/gradido/compare/1.1.1...1.2.0)
|
||||
|
||||
@ -219,9 +466,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Update Password Reset E-Mail Subject Encoding [`#579`](https://github.com/gradido/gradido/pull/579)
|
||||
- move decay between transactions into the transactions [`#483`](https://github.com/gradido/gradido/pull/483)
|
||||
- fix #591 [`#591`](https://github.com/gradido/gradido/issues/591)
|
||||
- fix style decay startblick [`cc7778b`](https://github.com/gradido/gradido/commit/cc7778b55d1baaa7be2d9440480e0fb27bb9a930)
|
||||
- Remove dynamic cast because it lead to errors again and agin (Poco::AutoPtr don't work correct with that) [`0db5912`](https://github.com/gradido/gradido/commit/0db5912a67158be8f313c01f06350f8339cb0e28)
|
||||
- Remove dynamic cast because it lead to errors again and agin (Poco::AutoPtr don't work correct with that) [`cee7d7a`](https://github.com/gradido/gradido/commit/cee7d7ac3c4c8c1f481cc3a87fb15422c858413b)
|
||||
|
||||
#### [1.1.1](https://github.com/gradido/gradido/compare/1.1.0...1.1.1)
|
||||
|
||||
@ -232,9 +476,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- update transfer email text [`#574`](https://github.com/gradido/gradido/pull/574)
|
||||
- update mysql because tuple has changed [`#576`](https://github.com/gradido/gradido/pull/576)
|
||||
- Login fix pending transactions [`#578`](https://github.com/gradido/gradido/pull/578)
|
||||
- add test to prevent bug in future [`630d667`](https://github.com/gradido/gradido/commit/630d667e996870a1bf9aa9586b0467d58419e525)
|
||||
- use standard path. add nginx example [`ac249b4`](https://github.com/gradido/gradido/commit/ac249b46830a8039aec52d30b48084b50a264b6f)
|
||||
- add autodeploy bash scripts [`f49cf4d`](https://github.com/gradido/gradido/commit/f49cf4d7f8054d87efa1e12055a7ef0c6d3b9872)
|
||||
|
||||
#### [1.1.0](https://github.com/gradido/gradido/compare/1.0.2...1.1.0)
|
||||
|
||||
@ -276,9 +517,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- login without hedera [`#478`](https://github.com/gradido/gradido/pull/478)
|
||||
- fix: Show Correct Version Number in Footer [`#475`](https://github.com/gradido/gradido/pull/475)
|
||||
- refactor: Remove Element-UI [`#476`](https://github.com/gradido/gradido/pull/476)
|
||||
- remove components Charts, Notification, SearchUser, ButtonCheckbox, Button RadioGroup, Breadcrumb [`159bff7`](https://github.com/gradido/gradido/commit/159bff71df20a5c48f93389b2f990f7fe54e53b9)
|
||||
- fix bug, update dockerfiles to use dependencies without grpc [`dedcebd`](https://github.com/gradido/gradido/commit/dedcebdb95ee0f3dfd2ad62074d4181af38476a2)
|
||||
- add warning to able to forward warnings from community server to client [`2fc3fe9`](https://github.com/gradido/gradido/commit/2fc3fe94a09bae199bf2f34f9df90e8fc3879c2b)
|
||||
|
||||
#### [1.0.2](https://github.com/gradido/gradido/compare/1.0.1...1.0.2)
|
||||
|
||||
@ -305,17 +543,12 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- add dynamic error email if transaction failed [`#452`](https://github.com/gradido/gradido/pull/452)
|
||||
- ceil the last decay [`#449`](https://github.com/gradido/gradido/pull/449)
|
||||
- feat: Raise Coverage of Frontend Unit Tets to 18% [`#447`](https://github.com/gradido/gradido/pull/447)
|
||||
- parse cpsp files automatic in build [`a4a12bb`](https://github.com/gradido/gradido/commit/a4a12bb62b4000e035ff15e17c5a5f5861653ff6)
|
||||
- translate german html encoded error messages to english and use gettext for automatic translation [`d339627`](https://github.com/gradido/gradido/commit/d33962736d94c1cb7a12ff775bc2c8d7505d646e)
|
||||
- 100% coverage of GddTransactionList [`96fb245`](https://github.com/gradido/gradido/commit/96fb245821c69f4d321204a663247d5eee60d92f)
|
||||
|
||||
#### [1.0.1](https://github.com/gradido/gradido/compare/1.0.0...1.0.1)
|
||||
|
||||
> 14 May 2021
|
||||
|
||||
- Login crash fix [`#444`](https://github.com/gradido/gradido/pull/444)
|
||||
- add try catch blocks to prevent login-server from crashing [`22ff220`](https://github.com/gradido/gradido/commit/22ff22072956f8b843037c75c5b16b7ff5d6a2a3)
|
||||
- fix [`14a4243`](https://github.com/gradido/gradido/commit/14a424347817b1fe6912a113bffd70e55d688112)
|
||||
|
||||
### [1.0.0](https://github.com/gradido/gradido/compare/0.9.4...1.0.0)
|
||||
|
||||
@ -435,9 +668,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Background color change [`#117`](https://github.com/gradido/gradido/pull/117)
|
||||
- Delete unused files [`#116`](https://github.com/gradido/gradido/pull/116)
|
||||
- store aufräumen teil 1 [`#115`](https://github.com/gradido/gradido/pull/115)
|
||||
- add migrations table for automatic table data migration [`40a9a8c`](https://github.com/gradido/gradido/commit/40a9a8c2b587f5bef0fcc54136ed7bd13dd91b2b)
|
||||
- update yarn.lock after running yarn install [`7f38c80`](https://github.com/gradido/gradido/commit/7f38c801213ad886e9d34a8d43b00ae423f5f2a0)
|
||||
- use new function for balance overview in old frontend, update balance in session on every php-request [`97c570c`](https://github.com/gradido/gradido/commit/97c570c08cc51ed17a69eb8be8d987f95f3c2ce0)
|
||||
|
||||
#### [0.9.4](https://github.com/gradido/gradido/compare/0.9.3...0.9.4)
|
||||
|
||||
@ -445,9 +675,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
|
||||
- Vue with nginx [`#84`](https://github.com/gradido/gradido/pull/84)
|
||||
- Build on run [`#103`](https://github.com/gradido/gradido/pull/103)
|
||||
- update debug docker to use dependencies container pushed to docker hub [`1f002f4`](https://github.com/gradido/gradido/commit/1f002f4ed0b12d4b2bf63efceabe546d0c5b58ea)
|
||||
- removed email tasks complete [`8a143be`](https://github.com/gradido/gradido/commit/8a143be8423d7bd894d4f512848895df8b9694b0)
|
||||
- build login-server on docker-compose up in a docker volume so it rebuild only neccessary parts if some c++ files have changed [`0da5279`](https://github.com/gradido/gradido/commit/0da527917523530186e6effe63dc001fc99bd3e3)
|
||||
|
||||
#### [0.9.3](https://github.com/gradido/gradido/compare/0.9.2...0.9.3)
|
||||
|
||||
@ -476,9 +703,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Add Feature in user search old frontend because Support has requested the feature long ago [`#56`](https://github.com/gradido/gradido/pull/56)
|
||||
- sprache angepasst, for login, pwd, sigin [`#54`](https://github.com/gradido/gradido/pull/54)
|
||||
- Improve workflows [`#53`](https://github.com/gradido/gradido/pull/53)
|
||||
- setup eslint with tougher rules [`1f13507`](https://github.com/gradido/gradido/commit/1f13507eacfd93c2248fb841de5f481c9eb1e6bd)
|
||||
- semicolon rule implemented [`6762a02`](https://github.com/gradido/gradido/commit/6762a028f2a3e4f2713b26bed81029defe686ad7)
|
||||
- dev meeting, bernd [`a99de7f`](https://github.com/gradido/gradido/commit/a99de7f5d1f7557c0877eae565aa4263d65aaaf3)
|
||||
|
||||
#### [0.9.2](https://github.com/gradido/gradido/compare/0.9.1...0.9.2)
|
||||
|
||||
@ -488,9 +712,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- Reload after login fixed [`#50`](https://github.com/gradido/gradido/pull/50)
|
||||
- Monorepo login server [`#48`](https://github.com/gradido/gradido/pull/48)
|
||||
- Stage0 [`#3`](https://github.com/gradido/gradido/pull/3)
|
||||
- Add auto-sign Transaction functionality [`5592275`](https://github.com/gradido/gradido/commit/55922753a7ffd9552be132501d744da491c409b5)
|
||||
- read in login the real client ip X-Real-IP from nginx forwarded not from community server [`512d307`](https://github.com/gradido/gradido/commit/512d307a19b955bb6e26ae8b274def354829b50f)
|
||||
- move check if all passwords allow direct into pwdValidation so it will work with every code which ask for password [`e2c38c1`](https://github.com/gradido/gradido/commit/e2c38c1a0fc25a4a2bc922c4bbc44d86b6d00d8b)
|
||||
|
||||
#### 0.9.1
|
||||
|
||||
@ -502,6 +723,3 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||
- [WIP] 2 create a dockerfile for the frontend application [`#6`](https://github.com/gradido/gradido/pull/6)
|
||||
- Master - first step [`#1`](https://github.com/gradido/gradido/pull/1)
|
||||
- Add docker compose [`#7`](https://github.com/gradido/gradido/pull/7)
|
||||
- style 404 side :) [`c7bdf89`](https://github.com/gradido/gradido/commit/c7bdf8978594b932615e48f9bb1c19d3c3bf3fcf)
|
||||
- publish workflow test [`df6f66f`](https://github.com/gradido/gradido/commit/df6f66ffe70baa9ed3f70b460a6c0c14011bb944)
|
||||
- many translations. translation structure [`bf68547`](https://github.com/gradido/gradido/commit/bf685479767d19c246c4d6abe3577dc3cb666346)
|
||||
|
||||
@ -60,8 +60,6 @@ docker-compose -f docker-compose.yml up
|
||||
- [frontend](./frontend) Wallet frontend
|
||||
- [backend](./backend) GraphQL & Business logic backend
|
||||
- [mariadb](./mariadb) Database backend
|
||||
- [login_server](./login_server) User credential storage & business logic backend
|
||||
- [community_server](./community_server/) Business logic backend
|
||||
|
||||
We are currently restructuring the service to reduce dependencies and unify business logic into one place. Furthermore the databases defined for each service will be unified into one.
|
||||
|
||||
|
||||
3
admin/.gitignore
vendored
3
admin/.gitignore
vendored
@ -2,7 +2,8 @@ node_modules/
|
||||
dist/
|
||||
.cache/
|
||||
|
||||
.env
|
||||
/.env
|
||||
/.env.bak
|
||||
|
||||
# coverage folder
|
||||
coverage/
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "0.1.0",
|
||||
"version": "1.6.6",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
@ -11,8 +11,8 @@
|
||||
"serve": "vue-cli-service serve --open",
|
||||
"dev": "yarn run serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "eslint --ext .js,.vue .",
|
||||
"test": "jest --coverage",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.vue .",
|
||||
"test": "TZ=UTC jest --coverage",
|
||||
"locales": "scripts/missing-keys.sh && scripts/sort.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -28,24 +28,22 @@
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-vue": "^2.0.2",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootstrap": "4.3.1",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"core-js": "^3.6.5",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"graphql": "^15.6.1",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.6.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "26.6.3",
|
||||
"moment": "^2.29.1",
|
||||
"portal-vue": "^2.1.7",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"stats-webpack-plugin": "^0.7.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-apollo": "^3.0.8",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-jest": "^3.0.7",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-toasted": "^1.1.28",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-persistedstate": "^4.1.0"
|
||||
},
|
||||
|
||||
BIN
admin/public/img/elopage_favicon.png
Normal file
BIN
admin/public/img/elopage_favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@ -13,3 +13,11 @@ export default {
|
||||
components: { defaultLayout },
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
.pointer:hover {
|
||||
background-color: rgb(216, 213, 213);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,21 +1,17 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
|
||||
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue()
|
||||
const toastSuccessMock = jest.fn()
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$toasted: {
|
||||
success: toastSuccessMock,
|
||||
error: toastErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -54,7 +50,7 @@ describe('ConfirmRegisterMailFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessMock).toBeCalledWith('unregister_mail.success')
|
||||
expect(toastSuccessSpy).toBeCalledWith('unregister_mail.success')
|
||||
})
|
||||
})
|
||||
|
||||
@ -66,7 +62,7 @@ describe('ConfirmRegisterMailFormular', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('unregister_mail.error')
|
||||
expect(toastErrorSpy).toBeCalledWith('unregister_mail.error')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
<template>
|
||||
<div class="component-confirm-register-mail">
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<div v-if="checked">{{ $t('unregister_mail.text_true', { date: dateLastSend }) }}</div>
|
||||
<div v-if="checked">{{ $t('unregister_mail.text_true') }}</div>
|
||||
<div v-else>
|
||||
{{ $t('unregister_mail.text_false', { date: dateLastSend, mail: email }) }}
|
||||
{{
|
||||
dateLastSend === ''
|
||||
? $t('unregister_mail.never_sent', { email })
|
||||
: $t('unregister_mail.text_false', { date: dateLastSend, email })
|
||||
}}
|
||||
|
||||
<!-- Using components -->
|
||||
<b-input-group :prepend="$t('unregister_mail.info')" class="mt-3">
|
||||
@ -44,10 +48,10 @@ export default {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$toasted.success(this.$t('unregister_mail.success', { email: this.email }))
|
||||
this.toastSuccess(this.$t('unregister_mail.success', { email: this.email }))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(this.$t('unregister_mail.error', { message: error.message }))
|
||||
this.toastError(this.$t('unregister_mail.error', { message: error.message }))
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,15 +1,45 @@
|
||||
<template>
|
||||
<div class="">
|
||||
<div>
|
||||
<hr />
|
||||
<br />
|
||||
<div class="text-center">
|
||||
{{ $t('gradido_admin_footer') }}
|
||||
<div><small>Version: 1.0.0</small></div>
|
||||
</div>
|
||||
<b-row align-v="center" class="mt-4 justify-content-lg-between">
|
||||
<b-col>
|
||||
<div class="copyright text-center text-lg-center text-muted">
|
||||
© {{ year }}
|
||||
<a
|
||||
:href="`https://gradido.net/${$i18n.locale}`"
|
||||
class="font-weight-bold ml-1"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('gradido_admin_footer') }}
|
||||
</a>
|
||||
|
|
||||
<a href="https://github.com/gradido/gradido/releases/latest" target="_blank">
|
||||
App version {{ version }}
|
||||
</a>
|
||||
<a
|
||||
v-if="hash"
|
||||
:href="'https://github.com/gradido/gradido/commit/' + hash"
|
||||
target="_blank"
|
||||
>
|
||||
({{ shortHash }})
|
||||
</a>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CONFIG from '../config'
|
||||
|
||||
export default {
|
||||
name: 'ContentFooter',
|
||||
data() {
|
||||
return {
|
||||
year: new Date().getFullYear(),
|
||||
version: CONFIG.APP_VERSION,
|
||||
hash: CONFIG.BUILD_COMMIT,
|
||||
shortHash: CONFIG.BUILD_COMMIT_SHORT,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,39 +1,25 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationFormular from './CreationFormular.vue'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
verifyLogin: {
|
||||
name: 'success',
|
||||
id: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreation: [0, 0, 0],
|
||||
},
|
||||
})
|
||||
const stateCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
$t: jest.fn((t, options) => (options ? [t, options] : t)),
|
||||
$d: jest.fn((d) => {
|
||||
const date = new Date(d)
|
||||
return date.toISOString().split('T')[0]
|
||||
}),
|
||||
$apollo: {
|
||||
query: apolloMock,
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$store: {
|
||||
@ -45,16 +31,17 @@ const mocks = {
|
||||
},
|
||||
},
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
type: '',
|
||||
creation: [],
|
||||
itemsMassCreation: {},
|
||||
}
|
||||
|
||||
const now = new Date(Date.now())
|
||||
const getCreationDate = (sub) => {
|
||||
const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0)
|
||||
return date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
describe('CreationFormular', () => {
|
||||
@ -73,21 +60,24 @@ describe('CreationFormular', () => {
|
||||
expect(wrapper.find('.component-creation-formular').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('server sends back moderator data', () => {
|
||||
it('called store commit with mocked data', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('moderator', { name: 'success', id: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('server throws error for moderator data call', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
apolloMock.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||
wrapper = Wrapper()
|
||||
describe('text and value form props', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = mount(CreationFormular, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData: {
|
||||
creationUserData: { memo: 'Memo from property', amount: 42 },
|
||||
...propsData,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has called store commit with fake data', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('moderator', { id: 0, name: 'Test Moderator' })
|
||||
it('has text taken from props', () => {
|
||||
expect(wrapper.vm.text).toBe('Memo from property')
|
||||
})
|
||||
|
||||
it('has value taken from props', () => {
|
||||
expect(wrapper.vm.value).toBe(42)
|
||||
})
|
||||
})
|
||||
|
||||
@ -96,68 +86,22 @@ describe('CreationFormular', () => {
|
||||
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
|
||||
})
|
||||
|
||||
describe('with mass creation', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setProps({ type: 'massCreation', creation: [200, 400, 600] })
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
})
|
||||
|
||||
describe('first radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
it('emits update-radio-selected with index 0', () => {
|
||||
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||
[expect.arrayContaining([0])],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('second radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
})
|
||||
|
||||
it('emits update-radio-selected with index 1', () => {
|
||||
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||
[expect.arrayContaining([1])],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('third radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
})
|
||||
|
||||
it('emits update-radio-selected with index 2', () => {
|
||||
expect(wrapper.emitted()['update-radio-selected']).toEqual([
|
||||
[expect.arrayContaining([2])],
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('with single creation', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
await wrapper.setProps({
|
||||
type: 'singleCreation',
|
||||
creation: [200, 400, 600],
|
||||
item: { email: 'benjamin@bluemchen.de' },
|
||||
})
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
await wrapper.find('input[type="number"]').setValue(90)
|
||||
})
|
||||
|
||||
describe('first radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
await wrapper.find('textarea').setValue('Test create coins')
|
||||
})
|
||||
|
||||
it('sets rangeMax to 200', () => {
|
||||
@ -170,18 +114,50 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
|
||||
it('sends ... to apollo', () => {
|
||||
expect(apolloMutateMock).toBeCalled()
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createPendingCreation,
|
||||
variables: {
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creationDate: getCreationDate(2),
|
||||
amount: 90,
|
||||
memo: 'Test create coins',
|
||||
moderator: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update-user-data', () => {
|
||||
expect(wrapper.emitted('update-user-data')).toEqual([
|
||||
[{ email: 'benjamin@bluemchen.de' }, [0, 0, 0]],
|
||||
])
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith([
|
||||
'creation_form.toasted',
|
||||
{ email: 'benjamin@bluemchen.de', value: '90' },
|
||||
])
|
||||
})
|
||||
|
||||
it('updates open creations in store', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 1)
|
||||
})
|
||||
|
||||
it('resets the form data', () => {
|
||||
expect(wrapper.vm.value).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendForm', () => {
|
||||
describe('sendForm with server error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
|
||||
@ -311,7 +287,7 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
|
||||
it('toast success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalled()
|
||||
expect(toastSuccessSpy).toBeCalled()
|
||||
})
|
||||
|
||||
it('store commit openCreationPlus', () => {
|
||||
@ -359,6 +335,124 @@ describe('CreationFormular', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mass creation with success', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreations: {
|
||||
success: true,
|
||||
successfulCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
failedCreation: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
await wrapper.setProps({
|
||||
type: 'massCreation',
|
||||
creation: [200, 400, 600],
|
||||
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
|
||||
})
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
await wrapper.find('textarea').setValue('Test mass create coins')
|
||||
await wrapper.find('input[type="number"]').setValue(200)
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createPendingCreations,
|
||||
variables: {
|
||||
pendingCreations: [
|
||||
{
|
||||
email: 'bob@baumeister.de',
|
||||
creationDate: getCreationDate(1),
|
||||
amount: 200,
|
||||
memo: 'Test mass create coins',
|
||||
moderator: 0,
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
creationDate: getCreationDate(1),
|
||||
amount: 200,
|
||||
memo: 'Test mass create coins',
|
||||
moderator: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('updates open creations in store', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 2)
|
||||
})
|
||||
|
||||
it('emits remove-all-bookmark', () => {
|
||||
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('mass creation with success but all failed', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createPendingCreations: {
|
||||
success: true,
|
||||
successfulCreation: [],
|
||||
failedCreation: ['bob@baumeister.de', 'bibi@bloxberg.de'],
|
||||
},
|
||||
},
|
||||
})
|
||||
await wrapper.setProps({
|
||||
type: 'massCreation',
|
||||
creation: [200, 400, 600],
|
||||
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
|
||||
})
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
await wrapper.find('textarea').setValue('Test mass create coins')
|
||||
await wrapper.find('input[type="number"]').setValue(200)
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('updates open creations in store', () => {
|
||||
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
|
||||
})
|
||||
|
||||
it('emits remove all bookmarks', () => {
|
||||
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emits toast failed creations with two emails', () => {
|
||||
expect(wrapper.emitted('toast-failed-creations')).toEqual([
|
||||
[['bob@baumeister.de', 'bibi@bloxberg.de']],
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('mass creation with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Oh no!',
|
||||
})
|
||||
await wrapper.setProps({
|
||||
type: 'massCreation',
|
||||
creation: [200, 400, 600],
|
||||
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
|
||||
})
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
await wrapper.find('textarea').setValue('Test mass create coins')
|
||||
await wrapper.find('input[type="number"]').setValue(200)
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,53 +3,19 @@
|
||||
{{ $t('creation_form.form') }}
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<b-form ref="creationForm">
|
||||
<b-row class="m-4">
|
||||
<div class="ml-4">
|
||||
<label>{{ $t('creation_form.select_month') }}</label>
|
||||
<b-col class="text-left">
|
||||
<b-form-radio
|
||||
id="beforeLastMonth"
|
||||
v-model="radioSelected"
|
||||
:value="beforeLastMonth"
|
||||
:disabled="creation[0] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(beforeLastMonth, 0, creation[0])"
|
||||
>
|
||||
<label for="beforeLastMonth">
|
||||
{{ beforeLastMonth.short }} {{ creation[0] != null ? creation[0] + ' GDD' : '' }}
|
||||
</label>
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-radio
|
||||
id="lastMonth"
|
||||
v-model="radioSelected"
|
||||
:value="lastMonth"
|
||||
:disabled="creation[1] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(lastMonth, 1, creation[1])"
|
||||
>
|
||||
<label for="lastMonth">
|
||||
{{ lastMonth.short }} {{ creation[1] != null ? creation[1] + ' GDD' : '' }}
|
||||
</label>
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-form-radio
|
||||
id="currentMonth"
|
||||
v-model="radioSelected"
|
||||
:value="currentMonth"
|
||||
:disabled="creation[2] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(currentMonth, 2, creation[2])"
|
||||
>
|
||||
<label for="currentMonth">
|
||||
{{ currentMonth.short }} {{ creation[2] != null ? creation[2] + ' GDD' : '' }}
|
||||
</label>
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
</div>
|
||||
<b-row class="ml-4">
|
||||
<b-form-radio-group
|
||||
v-model="selected"
|
||||
:options="radioOptions"
|
||||
value-field="item"
|
||||
text-field="name"
|
||||
name="month-selection"
|
||||
></b-form-radio-group>
|
||||
</b-row>
|
||||
|
||||
<b-row class="m-4" v-show="createdIndex != null">
|
||||
<b-row class="m-4" v-show="selected !== ''">
|
||||
<label>{{ $t('creation_form.select_value') }}</label>
|
||||
<div>
|
||||
<b-input-group prepend="GDD" append=".00">
|
||||
@ -60,7 +26,6 @@
|
||||
:max="rangeMax"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
|
||||
<b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
|
||||
<b-form-input
|
||||
type="range"
|
||||
@ -72,7 +37,7 @@
|
||||
</b-input-group>
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
<div class="m-4">
|
||||
<label>{{ $t('creation_form.enter_text') }}</label>
|
||||
<div>
|
||||
<b-form-textarea
|
||||
@ -83,9 +48,9 @@
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
</div>
|
||||
</b-row>
|
||||
</div>
|
||||
<b-row class="m-4">
|
||||
<b-col class="text-center">
|
||||
<b-col class="text-left">
|
||||
<b-button type="reset" variant="danger" @click="$refs.creationForm.reset()">
|
||||
{{ $t('creation_form.reset') }}
|
||||
</b-button>
|
||||
@ -98,18 +63,17 @@
|
||||
variant="success"
|
||||
class="test-submit"
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
:disabled="selected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
{{ $t('creation_form.update_creation') }}
|
||||
</b-button>
|
||||
|
||||
<b-button
|
||||
v-else
|
||||
type="button"
|
||||
variant="success"
|
||||
class="test-submit"
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
:disabled="selected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
{{ $t('creation_form.submit_creation') }}
|
||||
</b-button>
|
||||
@ -121,11 +85,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { verifyLogin } from '../graphql/verifyLogin'
|
||||
import { createPendingCreation } from '../graphql/createPendingCreation'
|
||||
import { createPendingCreations } from '../graphql/createPendingCreations'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
export default {
|
||||
name: 'CreationFormular',
|
||||
mixins: [creationMonths],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
@ -164,142 +129,97 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
radioSelected: '',
|
||||
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
|
||||
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
|
||||
rangeMin: 0,
|
||||
rangeMax: 1000,
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
long: this.$moment().format('YYYY-MM-DD'),
|
||||
year: this.$moment().format('YYYY'),
|
||||
},
|
||||
lastMonth: {
|
||||
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
|
||||
year: this.$moment().subtract(1, 'month').format('YYYY'),
|
||||
},
|
||||
beforeLastMonth: {
|
||||
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
|
||||
year: this.$moment().subtract(2, 'month').format('YYYY'),
|
||||
},
|
||||
submitObj: null,
|
||||
isdisabled: true,
|
||||
createdIndex: null,
|
||||
selected: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// Auswählen eines Zeitraumes
|
||||
updateRadioSelected(name, index, openCreation) {
|
||||
this.createdIndex = index
|
||||
updateRadioSelected(name) {
|
||||
// do we want to reset the memo everytime the month changes?
|
||||
this.text = this.$t('creation_form.creation_for') + ' ' + name.short + ' ' + name.year
|
||||
// Wenn Mehrfachschöpfung
|
||||
if (this.type === 'massCreation') {
|
||||
// An Creation.vue emitten und radioSelectedMass aktualisieren
|
||||
this.$emit('update-radio-selected', [name, index])
|
||||
} else if (this.type === 'singleCreation') {
|
||||
if (this.type === 'singleCreation') {
|
||||
this.rangeMin = 0
|
||||
// Der maximale offene Betrag an GDD die für ein User noch geschöpft werden kann
|
||||
this.rangeMax = openCreation
|
||||
this.rangeMax = name.creation
|
||||
}
|
||||
},
|
||||
submitCreation() {
|
||||
let submitObj = []
|
||||
if (this.type === 'massCreation') {
|
||||
// Die anzahl der Mitglieder aus der Mehrfachschöpfung
|
||||
const i = Object.keys(this.items).length
|
||||
// hinweis das eine Mehrfachschöpfung ausgeführt wird an (Anzahl der MItgleider an die geschöpft wird)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('SUBMIT CREATION => ' + this.type + ' >> für VIELE ' + i + ' Mitglieder')
|
||||
this.submitObj = []
|
||||
this.items.forEach((item) => {
|
||||
this.submitObj.push({
|
||||
submitObj.push({
|
||||
email: item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
creationDate: this.selected.date,
|
||||
amount: Number(this.value),
|
||||
memo: this.text,
|
||||
moderator: Number(this.$store.state.moderator.id),
|
||||
})
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('MehrfachSCHÖPFUNG ABSENDEN FÜR >> ' + i + ' Mitglieder')
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreations,
|
||||
variables: {
|
||||
pendingCreations: this.submitObj,
|
||||
pendingCreations: submitObj,
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
.then((result) => {
|
||||
const failedCreations = []
|
||||
this.$store.commit(
|
||||
'openCreationsPlus',
|
||||
result.data.createPendingCreations.successfulCreation.length,
|
||||
)
|
||||
if (result.data.createPendingCreations.failedCreation.length > 0) {
|
||||
result.data.createPendingCreations.failedCreation.forEach((failed) => {
|
||||
this.$toasted.error('Could not created PendingCreation for ' + failed)
|
||||
result.data.createPendingCreations.failedCreation.forEach((email) => {
|
||||
failedCreations.push(email)
|
||||
})
|
||||
}
|
||||
this.$emit('remove-all-bookmark')
|
||||
this.$emit('toast-failed-creations', failedCreations)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
} else if (this.type === 'singleCreation') {
|
||||
this.submitObj = {
|
||||
submitObj = {
|
||||
email: this.item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
creationDate: this.selected.date,
|
||||
amount: Number(this.value),
|
||||
memo: this.text,
|
||||
moderator: Number(this.$store.state.moderator.id),
|
||||
}
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createPendingCreation,
|
||||
variables: this.submitObj,
|
||||
variables: submitObj,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.createPendingCreation)
|
||||
this.$toasted.success(
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.toastSuccess(
|
||||
this.$t('creation_form.toasted', {
|
||||
value: this.value,
|
||||
email: this.item.email,
|
||||
}),
|
||||
)
|
||||
this.$store.commit('openCreationsPlus', 1)
|
||||
this.submitObj = null
|
||||
this.createdIndex = null
|
||||
// das creation Formular reseten
|
||||
// what is this? Tests says that this.text is not reseted
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.submitObj = null
|
||||
// das creation Formular reseten
|
||||
this.toastError(error.message)
|
||||
this.$refs.creationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
}
|
||||
},
|
||||
searchModeratorData() {
|
||||
this.$apollo
|
||||
.query({ query: verifyLogin })
|
||||
.then((result) => {
|
||||
this.$store.commit('moderator', result.data.verifyLogin)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$store.commit('moderator', { id: 0, name: 'Test Moderator' })
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.searchModeratorData()
|
||||
watch: {
|
||||
selected() {
|
||||
this.updateRadioSelected(this.selected)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationTransactionListFormular from './CreationTransactionListFormular.vue'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -15,7 +16,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
decayDuration: 0,
|
||||
memo: 'Testing',
|
||||
transactionId: 1,
|
||||
name: 'Bibi',
|
||||
name: 'Gradido Akademie',
|
||||
email: 'bibi@bloxberg.de',
|
||||
date: new Date(),
|
||||
decay: {
|
||||
@ -34,7 +35,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
decayDuration: 0,
|
||||
memo: 'Testing 2',
|
||||
transactionId: 2,
|
||||
name: 'Bibi',
|
||||
name: 'Gradido Akademie',
|
||||
email: 'bibi@bloxberg.de',
|
||||
date: new Date(),
|
||||
decay: {
|
||||
@ -50,20 +51,17 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
},
|
||||
})
|
||||
|
||||
const toastedErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$d: jest.fn((t) => t),
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
userId: 1,
|
||||
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
||||
}
|
||||
|
||||
describe('CreationTransactionListFormular', () => {
|
||||
@ -107,7 +105,7 @@ describe('CreationTransactionListFormular', () => {
|
||||
})
|
||||
|
||||
it('toast error', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('OUCH!')
|
||||
expect(toastErrorSpy).toBeCalledWith('OUCH!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="component-creation-transaction-list">
|
||||
{{ $t('transactionlist.title') }}
|
||||
<b-table striped hover :items="items"></b-table>
|
||||
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -13,6 +13,35 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
{
|
||||
key: 'date',
|
||||
label: this.$t('transactionlist.date'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'balance',
|
||||
label: this.$t('transactionlist.amount'),
|
||||
formatter: (value, key, item) => {
|
||||
return `${value} GDD`
|
||||
},
|
||||
},
|
||||
{ key: 'name', label: this.$t('transactionlist.community') },
|
||||
{ key: 'memo', label: this.$t('transactionlist.memo') },
|
||||
{
|
||||
key: 'decay',
|
||||
label: this.$t('transactionlist.decay'),
|
||||
formatter: (value, key, item) => {
|
||||
if (value && value.balance >= 0) {
|
||||
return value.balance
|
||||
} else {
|
||||
return '0'
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
@ -33,7 +62,7 @@ export default {
|
||||
this.items = result.data.transactionList.transactions.filter((t) => t.type === 'creation')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
248
admin/src/components/DeletedUserFormular.spec.js
Normal file
248
admin/src/components/DeletedUserFormular.spec.js
Normal file
@ -0,0 +1,248 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import DeletedUserFormular from './DeletedUserFormular.vue'
|
||||
import { deleteUser } from '../graphql/deleteUser'
|
||||
import { unDeleteUser } from '../graphql/unDeleteUser'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const date = new Date()
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
deleteUser: date,
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 0,
|
||||
name: 'test moderator',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
item: {},
|
||||
}
|
||||
|
||||
describe('DeletedUserFormular', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(DeletedUserFormular, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.delete-user-formular', () => {
|
||||
expect(wrapper.find('.deleted-user-formular').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete self', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 0,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a text that you cannot delete yourself', () => {
|
||||
expect(wrapper.text()).toBe('removeNotSelf')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete other user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "delete_user"', () => {
|
||||
expect(wrapper.text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has the button text "delete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('delete_user')
|
||||
})
|
||||
|
||||
describe('confirm delete with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('recover user', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({
|
||||
item: {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a checkbox', () => {
|
||||
expect(wrapper.find('input[type="checkbox"]').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows the text "undelete_user"', () => {
|
||||
expect(wrapper.text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('click on checkbox', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
unDeleteUser: null,
|
||||
},
|
||||
})
|
||||
await wrapper.find('input[type="checkbox"]').setChecked()
|
||||
})
|
||||
|
||||
it('has a confirmation button', () => {
|
||||
expect(wrapper.find('button').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has the button text "undelete_user"', () => {
|
||||
expect(wrapper.find('button').text()).toBe('undelete_user')
|
||||
})
|
||||
|
||||
describe('confirm recover with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: 1,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update deleted At', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
{
|
||||
userId: 1,
|
||||
deletedAt: null,
|
||||
},
|
||||
]),
|
||||
]),
|
||||
)
|
||||
})
|
||||
|
||||
it('unchecks the checkbox', () => {
|
||||
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm recover with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('click on checkbox again', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input[type="checkbox"]').setChecked(false)
|
||||
})
|
||||
|
||||
it('has no confirmation button anymore', () => {
|
||||
expect(wrapper.find('button').exists()).toBeFalsy()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
85
admin/src/components/DeletedUserFormular.vue
Normal file
85
admin/src/components/DeletedUserFormular.vue
Normal file
@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="deleted-user-formular">
|
||||
<div v-if="item.userId === $store.state.moderator.id" class="mt-5 mb-5">
|
||||
{{ $t('removeNotSelf') }}
|
||||
</div>
|
||||
<div v-else class="mt-5">
|
||||
<b-form-checkbox switch size="lg" v-model="checked">
|
||||
<div>{{ item.deletedAt ? $t('undelete_user') : $t('delete_user') }}</div>
|
||||
</b-form-checkbox>
|
||||
|
||||
<div class="mt-3 mb-5">
|
||||
<b-button v-if="checked && item.deletedAt === null" variant="danger" @click="deleteUser">
|
||||
{{ $t('delete_user') }}
|
||||
</b-button>
|
||||
<b-button v-if="checked && item.deletedAt !== null" variant="success" @click="unDeleteUser">
|
||||
{{ $t('undelete_user') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { deleteUser } from '../graphql/deleteUser'
|
||||
import { unDeleteUser } from '../graphql/unDeleteUser'
|
||||
|
||||
export default {
|
||||
name: 'DeletedUser',
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
checked: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteUser() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteUser,
|
||||
variables: {
|
||||
userId: this.item.userId,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('updateDeletedAt', {
|
||||
userId: this.item.userId,
|
||||
deletedAt: result.data.deleteUser,
|
||||
})
|
||||
this.checked = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
unDeleteUser() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: unDeleteUser,
|
||||
variables: {
|
||||
userId: this.item.userId,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.toastSuccess(this.$t('user_recovered'))
|
||||
this.$emit('updateDeletedAt', {
|
||||
userId: this.item.userId,
|
||||
deletedAt: result.data.unDeleteUser,
|
||||
})
|
||||
this.checked = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.input-group-text {
|
||||
background-color: rgb(255, 252, 205);
|
||||
}
|
||||
</style>
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import EditCreationFormular from './EditCreationFormular.vue'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -7,28 +8,21 @@ const apolloMutateMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
updatePendingCreation: {
|
||||
creation: [0, 0, 0],
|
||||
amount: 500,
|
||||
date: new Date(),
|
||||
memo: 'qwertzuiopasdfghjkl',
|
||||
memo: 'Test Schöpfung 2',
|
||||
moderator: 0,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const stateCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
$d: jest.fn((d) => {
|
||||
const date = new Date(d)
|
||||
return date.toISOString().split('T')[0]
|
||||
}),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
@ -42,10 +36,12 @@ const mocks = {
|
||||
},
|
||||
commit: stateCommitMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
}
|
||||
|
||||
const now = new Date(Date.now())
|
||||
const getCreationDate = (sub) => {
|
||||
const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0)
|
||||
return date.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -53,7 +49,7 @@ const propsData = {
|
||||
creationUserData: {
|
||||
memo: 'Test schöpfung 1',
|
||||
amount: 100,
|
||||
date: '2021-12-01',
|
||||
date: getCreationDate(0),
|
||||
},
|
||||
item: {
|
||||
id: 0,
|
||||
@ -82,196 +78,79 @@ describe('EditCreationFormular', () => {
|
||||
expect(wrapper.findAll('input[type="radio"]').length).toBe(3)
|
||||
})
|
||||
|
||||
describe('with single creation', () => {
|
||||
it('has the third radio button checked', () => {
|
||||
expect(wrapper.findAll('input[type="radio"]').at(0).element.checked).toBeFalsy()
|
||||
expect(wrapper.findAll('input[type="radio"]').at(1).element.checked).toBeFalsy()
|
||||
expect(wrapper.findAll('input[type="radio"]').at(2).element.checked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has rangeMax of 700', () => {
|
||||
expect(wrapper.find('input[type="number"]').attributes('max')).toBe('700')
|
||||
})
|
||||
|
||||
describe('change and save memo and value with success', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.setProps({ creation: [200, 400, 600] })
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
await wrapper.find('input[type="number"]').setValue(500)
|
||||
await wrapper.find('textarea').setValue('Test Schöpfung 2')
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
describe('first radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
})
|
||||
|
||||
it('sets rangeMax to 200', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(200)
|
||||
})
|
||||
|
||||
describe('sendForm', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('sends ... to apollo', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
amount: 90,
|
||||
creationDate: 'YYYY-MM-01',
|
||||
email: 'bob@baumeister.de',
|
||||
id: 0,
|
||||
memo: 'Test create coins',
|
||||
moderator: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('emits update-user-data', () => {
|
||||
expect(wrapper.emitted('update-user-data')).toBeTruthy()
|
||||
expect(wrapper.emitted('update-user-data')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 0,
|
||||
email: 'bob@baumeister.de',
|
||||
},
|
||||
[0, 0, 0],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('toast success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('creation_form.toasted_update')
|
||||
})
|
||||
|
||||
describe('sendForm with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
await wrapper.setProps({ type: 'singleCreation', creation: [200, 400, 600] })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
await wrapper.setData({ rangeMin: 100 })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toast error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
id: 0,
|
||||
email: 'bob@baumeister.de',
|
||||
creationDate: getCreationDate(0),
|
||||
amount: 500,
|
||||
memo: 'Test Schöpfung 2',
|
||||
moderator: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('second radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
})
|
||||
|
||||
it('sets rangeMin to 0', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(0)
|
||||
})
|
||||
|
||||
it('sets rangeMax to 400', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(400)
|
||||
})
|
||||
|
||||
describe('sendForm', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('sends ... to apollo', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
amount: 90,
|
||||
creationDate: 'YYYY-MM-01',
|
||||
email: 'bob@baumeister.de',
|
||||
id: 0,
|
||||
memo: 'Test create coins',
|
||||
moderator: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('sendForm with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
await wrapper.setProps({ creation: [200, 400, 600] })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 100 })
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toast error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
it('emits update-user-data', () => {
|
||||
expect(wrapper.emitted('update-user-data')).toEqual([
|
||||
[
|
||||
{
|
||||
id: 0,
|
||||
email: 'bob@baumeister.de',
|
||||
},
|
||||
[0, 0, 0],
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
describe('third radio button', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
})
|
||||
it('emits update-creation-data', () => {
|
||||
expect(wrapper.emitted('update-creation-data')).toEqual([
|
||||
[
|
||||
{
|
||||
amount: 500,
|
||||
date: expect.any(Date),
|
||||
memo: 'Test Schöpfung 2',
|
||||
moderator: 0,
|
||||
row: expect.any(Object),
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it('sets rangeMin to 180', () => {
|
||||
expect(wrapper.vm.rangeMin).toBe(180)
|
||||
})
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_update')
|
||||
})
|
||||
})
|
||||
|
||||
it('sets rangeMax to 700', () => {
|
||||
expect(wrapper.vm.rangeMax).toBe(700)
|
||||
})
|
||||
describe('change and save memo and value with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
|
||||
await wrapper.find('input[type="number"]').setValue(500)
|
||||
await wrapper.find('textarea').setValue('Test Schöpfung 2')
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
describe('sendForm with success', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('sends ... to apollo', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
amount: 90,
|
||||
creationDate: 'YYYY-MM-DD',
|
||||
email: 'bob@baumeister.de',
|
||||
id: 0,
|
||||
memo: 'Test create coins',
|
||||
moderator: 0,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sendForm with error', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
apolloMutateMock.mockRejectedValue({
|
||||
message: 'Ouch!',
|
||||
})
|
||||
wrapper = Wrapper()
|
||||
await wrapper.setProps({ creation: [200, 400, 600] })
|
||||
await wrapper.setData({ text: 'Test create coins' })
|
||||
await wrapper.setData({ value: 90 })
|
||||
await wrapper.findAll('input[type="radio"]').at(2).setChecked()
|
||||
await wrapper.setData({ rangeMin: 180 })
|
||||
await wrapper.find('.test-submit').trigger('click')
|
||||
})
|
||||
|
||||
it('toast error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Oh no!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,68 +2,19 @@
|
||||
<div class="component-edit-creation-formular">
|
||||
<div class="shadow p-3 mb-5 bg-white rounded">
|
||||
<b-form ref="updateCreationForm">
|
||||
<b-row class="m-4">
|
||||
<div class="ml-4">
|
||||
<label>{{ $t('creation_form.select_month') }}</label>
|
||||
<b-col class="text-left">
|
||||
<b-form-radio
|
||||
id="beforeLastMonth"
|
||||
v-model="radioSelected"
|
||||
:value="beforeLastMonth"
|
||||
:disabled="selectedOpenCreationAmount[0] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(beforeLastMonth, 0, selectedOpenCreationAmount[0])"
|
||||
>
|
||||
<label for="beforeLastMonth">
|
||||
{{ beforeLastMonth.short }}
|
||||
{{
|
||||
selectedOpenCreationAmount[0] != null
|
||||
? selectedOpenCreationAmount[0] + ' GDD'
|
||||
: ''
|
||||
}}
|
||||
</label>
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-radio
|
||||
id="lastMonth"
|
||||
v-model="radioSelected"
|
||||
:value="lastMonth"
|
||||
:disabled="selectedOpenCreationAmount[1] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(lastMonth, 1, selectedOpenCreationAmount[1])"
|
||||
>
|
||||
<label for="lastMonth">
|
||||
{{ lastMonth.short }}
|
||||
{{
|
||||
selectedOpenCreationAmount[1] != null
|
||||
? selectedOpenCreationAmount[1] + ' GDD'
|
||||
: ''
|
||||
}}
|
||||
</label>
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-form-radio
|
||||
id="currentMonth"
|
||||
v-model="radioSelected"
|
||||
:value="currentMonth"
|
||||
:disabled="selectedOpenCreationAmount[2] === 0"
|
||||
size="lg"
|
||||
@change="updateRadioSelected(currentMonth, 2, selectedOpenCreationAmount[2])"
|
||||
>
|
||||
<label for="currentMonth">
|
||||
{{ currentMonth.short }}
|
||||
{{
|
||||
selectedOpenCreationAmount[2] != null
|
||||
? selectedOpenCreationAmount[2] + ' GDD'
|
||||
: ''
|
||||
}}
|
||||
</label>
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
</div>
|
||||
<b-row class="m-4">
|
||||
<b-form-radio-group
|
||||
v-model="selected"
|
||||
:options="radioOptions"
|
||||
value-field="item"
|
||||
text-field="name"
|
||||
name="month-selection"
|
||||
></b-form-radio-group>
|
||||
</b-row>
|
||||
<div class="m-4">
|
||||
<label>{{ $t('creation_form.select_value') }}</label>
|
||||
<div>
|
||||
<b-input-group prepend="GDD" append=".00">
|
||||
@ -74,7 +25,6 @@
|
||||
:max="rangeMax"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
|
||||
<b-input-group prepend="0" :append="String(rangeMax)" class="mt-3">
|
||||
<b-form-input
|
||||
type="range"
|
||||
@ -85,8 +35,8 @@
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</b-row>
|
||||
<b-row class="m-4">
|
||||
</div>
|
||||
<div class="m-4">
|
||||
<label>{{ $t('creation_form.enter_text') }}</label>
|
||||
<div>
|
||||
<b-form-textarea
|
||||
@ -97,9 +47,9 @@
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
</div>
|
||||
</b-row>
|
||||
</div>
|
||||
<b-row class="m-4">
|
||||
<b-col class="text-center">
|
||||
<b-col class="text-left">
|
||||
<b-button type="reset" variant="danger" @click="$refs.updateCreationForm.reset()">
|
||||
{{ $t('creation_form.reset') }}
|
||||
</b-button>
|
||||
@ -111,7 +61,7 @@
|
||||
variant="success"
|
||||
class="test-submit"
|
||||
@click="submitCreation"
|
||||
:disabled="radioSelected === '' || value <= 0 || text.length < 10"
|
||||
:disabled="selected === '' || value <= 0 || text.length < 10"
|
||||
>
|
||||
{{ $t('creation_form.update_creation') }}
|
||||
</b-button>
|
||||
@ -124,8 +74,11 @@
|
||||
</template>
|
||||
<script>
|
||||
import { updatePendingCreation } from '../graphql/updatePendingCreation'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
|
||||
export default {
|
||||
name: 'EditCreationFormular',
|
||||
mixins: [creationMonths],
|
||||
props: {
|
||||
item: {
|
||||
type: Object,
|
||||
@ -149,51 +102,26 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
radioSelected: '',
|
||||
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
|
||||
value: !this.creationUserData.amount ? 0 : this.creationUserData.amount,
|
||||
rangeMin: 0,
|
||||
rangeMax: 1000,
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
long: this.$moment().format('YYYY-MM-DD'),
|
||||
},
|
||||
lastMonth: {
|
||||
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(1, 'month').format('YYYY-MM') + '-01',
|
||||
},
|
||||
beforeLastMonth: {
|
||||
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||
long: this.$moment().subtract(2, 'month').format('YYYY-MM') + '-01',
|
||||
},
|
||||
submitObj: null,
|
||||
isdisabled: true,
|
||||
createdIndex: null,
|
||||
selectedOpenCreationAmount: {},
|
||||
selected: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateRadioSelected(name, index, openCreation) {
|
||||
this.createdIndex = index
|
||||
this.rangeMin = 0
|
||||
this.rangeMax = this.creation[index]
|
||||
},
|
||||
submitCreation() {
|
||||
this.submitObj = {
|
||||
id: this.item.id,
|
||||
email: this.item.email,
|
||||
creationDate: this.radioSelected.long,
|
||||
amount: Number(this.value),
|
||||
memo: this.text,
|
||||
moderator: Number(this.$store.state.moderator.id),
|
||||
}
|
||||
|
||||
// hinweis das eine ein einzelne Schöpfung abgesendet wird an (email)
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: updatePendingCreation,
|
||||
variables: this.submitObj,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
email: this.item.email,
|
||||
creationDate: this.selected.date,
|
||||
amount: Number(this.value),
|
||||
memo: this.text,
|
||||
moderator: Number(this.$store.state.moderator.id),
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit('update-user-data', this.item, result.data.updatePendingCreation.creation)
|
||||
@ -204,22 +132,19 @@ export default {
|
||||
moderator: Number(result.data.updatePendingCreation.moderator),
|
||||
row: this.row,
|
||||
})
|
||||
this.$toasted.success(
|
||||
this.toastSuccess(
|
||||
this.$t('creation_form.toasted_update', {
|
||||
value: this.value,
|
||||
email: this.item.email,
|
||||
}),
|
||||
)
|
||||
this.submitObj = null
|
||||
this.createdIndex = null
|
||||
// das creation Formular reseten
|
||||
this.$refs.updateCreationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
this.value = 0
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.submitObj = null
|
||||
this.toastError(error.message)
|
||||
// das creation Formular reseten
|
||||
this.$refs.updateCreationForm.reset()
|
||||
// Den geschöpften Wert auf o setzen
|
||||
@ -229,25 +154,10 @@ export default {
|
||||
},
|
||||
created() {
|
||||
if (this.creationUserData.date) {
|
||||
switch (this.$moment(this.creationUserData.date).format('MMMM')) {
|
||||
case this.currentMonth.short:
|
||||
this.createdIndex = 2
|
||||
this.radioSelected = this.currentMonth
|
||||
break
|
||||
case this.lastMonth.short:
|
||||
this.createdIndex = 1
|
||||
this.radioSelected = this.lastMonth
|
||||
break
|
||||
case this.beforeLastMonth.short:
|
||||
this.createdIndex = 0
|
||||
this.radioSelected = this.beforeLastMonth
|
||||
break
|
||||
default:
|
||||
throw new Error('Something went wrong')
|
||||
}
|
||||
this.selectedOpenCreationAmount[this.createdIndex] =
|
||||
this.creation[this.createdIndex] + this.creationUserData.amount
|
||||
this.rangeMax = this.selectedOpenCreationAmount[this.createdIndex]
|
||||
const month = this.$d(new Date(this.creationUserData.date), 'month')
|
||||
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
|
||||
this.selected = this.radioOptions[index].item
|
||||
this.rangeMax = this.creation[index] + this.creationUserData.amount
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@ -3,11 +3,15 @@ import NotFoundPage from './NotFoundPage'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
}
|
||||
|
||||
describe('NotFoundPage', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(NotFoundPage, { localVue })
|
||||
return mount(NotFoundPage, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -18,5 +22,9 @@ describe('NotFoundPage', () => {
|
||||
it('has a svg', () => {
|
||||
expect(wrapper.find('svg').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has a back button', () => {
|
||||
expect(wrapper.find('.test-back').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<div class="header py-1 py-lg-1 pt-lg-3">
|
||||
<b-container>
|
||||
<div class="header-body text-center mb-3">
|
||||
<a href="login" to="login">
|
||||
<a href="#!" @click="goback">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-12 mt-5 mb-5">
|
||||
@ -1185,6 +1185,11 @@
|
||||
</div>
|
||||
</b-container>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<b-button class="test-back" variant="light" @click="goback">
|
||||
{{ $t('back') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -1213,6 +1218,11 @@ export default {
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goback() {
|
||||
this.$router.go(-1)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
|
||||
31
admin/src/components/Overlay.spec.js
Normal file
31
admin/src/components/Overlay.spec.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Overlay from './Overlay.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const propsData = {
|
||||
item: {},
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => String(d)),
|
||||
}
|
||||
|
||||
describe('Overlay', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Overlay, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-overlay', () => {
|
||||
expect(wrapper.find('.component-overlay').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
})
|
||||
67
admin/src/components/Overlay.vue
Normal file
67
admin/src/components/Overlay.vue
Normal file
@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="component-overlay">
|
||||
<b-jumbotron class="bg-light p-4">
|
||||
<template #header>{{ $t('overlay.confirm.title') }}</template>
|
||||
|
||||
<template #lead>
|
||||
<b-row class="mt-4">
|
||||
<b-col class="col-3">{{ $t('transactionlist.amount') }}</b-col>
|
||||
<b-col class="h3">
|
||||
<b>{{ item.amount }} GDD</b>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="col-3">{{ $t('creation_for_month') }}</b-col>
|
||||
<b-col class="h3">
|
||||
{{ $d(new Date(item.date), 'month') }} {{ $d(new Date(item.date), 'year') }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="col-3">{{ $t('transactionlist.memo') }}</b-col>
|
||||
<b-col>{{ item.memo }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-3">
|
||||
<b-col class="col-3">{{ $t('name') }}</b-col>
|
||||
<b-col>{{ item.firstName }} {{ item.lastName }}</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col class="col-3">{{ $t('e_mail') }}</b-col>
|
||||
<b-col>{{ item.email }}</b-col>
|
||||
</b-row>
|
||||
</template>
|
||||
|
||||
<hr class="my-4" />
|
||||
<p>{{ $t('overlay.confirm.text') }}</p>
|
||||
<p>
|
||||
{{ $t('overlay.confirm.question') }}
|
||||
</p>
|
||||
<b-container>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-button size="md" variant="danger" class="m-3" @click="$emit('overlay-cancel')">
|
||||
{{ $t('overlay.confirm.cancel') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
size="md"
|
||||
variant="success"
|
||||
class="m-3 text-right"
|
||||
@click="$emit('confirm-creation', item)"
|
||||
>
|
||||
{{ $t('overlay.confirm.yes') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
</b-jumbotron>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'overlay',
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<b-card class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
|
||||
<b-row class="mb-2">
|
||||
<b-col></b-col>
|
||||
</b-row>
|
||||
<slot :name="slotName" />
|
||||
<b-button size="sm" @click="$emit('row-toogle-details', row, index)">
|
||||
<b-button size="sm" @click="$emit('row-toggle-details', row, index)">
|
||||
<b-icon
|
||||
:icon="type === 'PageCreationConfirm' ? 'x' : 'eye-slash-fill'"
|
||||
aria-label="Help"
|
||||
|
||||
86
admin/src/components/Tables/OpenCreationsTable.vue
Normal file
86
admin/src/components/Tables/OpenCreationsTable.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="component-open-creations-table">
|
||||
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button
|
||||
variant="danger"
|
||||
size="md"
|
||||
@click="$emit('remove-creation', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button variant="info" size="md" @click="rowToggleDetails(row, 0)" class="mr-2">
|
||||
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(confirm)="row">
|
||||
<b-button variant="success" size="md" @click="$emit('show-overlay', row.item)" class="mr-2">
|
||||
<b-icon icon="check" scale="2" variant=""></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #row-details="row">
|
||||
<row-details
|
||||
:row="row"
|
||||
type="show-creation"
|
||||
slotName="show-creation"
|
||||
:index="0"
|
||||
@row-toggle-details="rowToggleDetails"
|
||||
>
|
||||
<template #show-creation>
|
||||
<div>
|
||||
<edit-creation-formular
|
||||
type="singleCreation"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:row="row"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</row-details>
|
||||
</template>
|
||||
</b-table-lite>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
|
||||
import RowDetails from '../RowDetails.vue'
|
||||
import EditCreationFormular from '../EditCreationFormular.vue'
|
||||
|
||||
export default {
|
||||
name: 'OpenCreationsTable',
|
||||
mixins: [toggleRowDetails],
|
||||
components: {
|
||||
EditCreationFormular,
|
||||
RowDetails,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateCreationData(data) {
|
||||
this.creationUserData.amount = data.amount
|
||||
this.creationUserData.date = data.date
|
||||
this.creationUserData.memo = data.memo
|
||||
this.creationUserData.moderator = data.moderator
|
||||
data.row.toggleDetails()
|
||||
},
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
125
admin/src/components/Tables/SearchUserTable.spec.js
Normal file
125
admin/src/components/Tables/SearchUserTable.spec.js
Normal file
@ -0,0 +1,125 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SearchUserTable from './SearchUserTable.vue'
|
||||
|
||||
const date = new Date()
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({})
|
||||
|
||||
const propsData = {
|
||||
items: [
|
||||
{
|
||||
userId: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
emailChecked: true,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: true,
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
creation: [0, 0, 0],
|
||||
emailChecked: true,
|
||||
},
|
||||
{
|
||||
userId: 4,
|
||||
firstName: 'New',
|
||||
lastName: 'User',
|
||||
email: 'new@user.ch',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: false,
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{ key: 'email', label: 'e_mail' },
|
||||
{ key: 'firstName', label: 'firstname' },
|
||||
{ key: 'lastName', label: 'lastname' },
|
||||
{
|
||||
key: 'creation',
|
||||
label: 'creationLabel',
|
||||
formatter: (value, key, item) => {
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
{ key: 'status', label: 'status' },
|
||||
],
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 0,
|
||||
name: 'test moderator',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('SearchUserTable', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(SearchUserTable, { localVue, mocks, propsData })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a table with four rows', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(4)
|
||||
})
|
||||
|
||||
describe('show row details', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('tbody > tr').at(1).trigger('click')
|
||||
})
|
||||
|
||||
describe('deleted at', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('div.deleted-user-formular').vm.$emit('updateDeletedAt', {
|
||||
userId: 1,
|
||||
deletedAt: date,
|
||||
})
|
||||
})
|
||||
|
||||
it('emits updateDeletedAt', () => {
|
||||
expect(wrapper.emitted('updateDeletedAt')).toEqual([[1, date]])
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateUserData', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.find('div.component-creation-formular')
|
||||
.vm.$emit('update-user-data', propsData.items[1], [250, 500, 750])
|
||||
})
|
||||
|
||||
it('updates the item', () => {
|
||||
expect(wrapper.vm.items[1].creation).toEqual([250, 500, 750])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
141
admin/src/components/Tables/SearchUserTable.vue
Normal file
141
admin/src/components/Tables/SearchUserTable.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="search-user-table">
|
||||
<b-table
|
||||
tbody-tr-class="pointer"
|
||||
:items="myItems"
|
||||
:fields="fields"
|
||||
caption-top
|
||||
striped
|
||||
hover
|
||||
stacked="md"
|
||||
select-mode="single"
|
||||
selectableonRowSelected
|
||||
@row-clicked="onRowClicked"
|
||||
>
|
||||
<template #cell(creation)="data">
|
||||
<div v-html="data.value"></div>
|
||||
</template>
|
||||
|
||||
<template #cell(status)="row">
|
||||
<div class="text-right">
|
||||
<b-avatar v-if="row.item.deletedAt" class="mr-3" variant="light">
|
||||
<b-iconstack font-scale="2">
|
||||
<b-icon stacked icon="person" variant="info" scale="0.75"></b-icon>
|
||||
<b-icon stacked icon="slash-circle" variant="danger"></b-icon>
|
||||
</b-iconstack>
|
||||
</b-avatar>
|
||||
<span v-if="!row.item.deletedAt">
|
||||
<b-avatar
|
||||
v-if="!row.item.emailChecked"
|
||||
icon="envelope"
|
||||
class="align-center mr-3"
|
||||
variant="danger"
|
||||
></b-avatar>
|
||||
|
||||
<b-avatar
|
||||
v-if="!row.item.hasElopage"
|
||||
variant="danger"
|
||||
class="mr-3"
|
||||
src="img/elopage_favicon.png"
|
||||
></b-avatar>
|
||||
</span>
|
||||
<b-icon
|
||||
variant="dark"
|
||||
:icon="row.detailsShowing ? 'caret-up-fill' : 'caret-down'"
|
||||
:title="row.item.enabled ? $t('enabled') : $t('deleted')"
|
||||
></b-icon>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #row-details="row">
|
||||
<b-card ref="rowDetails" class="shadow-lg pl-3 pr-3 mb-5 bg-white rounded">
|
||||
<creation-formular
|
||||
v-if="!row.item.deletedAt"
|
||||
type="singleCreation"
|
||||
pagetype="singleCreation"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:creationUserData="creationUserData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
<div v-else>{{ $t('userIsDeleted') }}</div>
|
||||
<confirm-register-mail-formular
|
||||
v-if="!row.item.deletedAt"
|
||||
:checked="row.item.emailChecked"
|
||||
:email="row.item.email"
|
||||
:dateLastSend="
|
||||
row.item.emailConfirmationSend
|
||||
? $d(new Date(row.item.emailConfirmationSend), 'long')
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<creation-transaction-list-formular
|
||||
v-if="!row.item.deletedAt"
|
||||
:userId="row.item.userId"
|
||||
/>
|
||||
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
|
||||
</b-card>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CreationFormular from '../CreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionListFormular from '../CreationTransactionListFormular.vue'
|
||||
import DeletedUserFormular from '../DeletedUserFormular.vue'
|
||||
|
||||
export default {
|
||||
name: 'SearchUserTable',
|
||||
components: {
|
||||
CreationFormular,
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionListFormular,
|
||||
DeletedUserFormular,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
creationUserData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
updateDeletedAt({ userId, deletedAt }) {
|
||||
this.$emit('updateDeletedAt', userId, deletedAt)
|
||||
},
|
||||
async onRowClicked(item) {
|
||||
const status = this.myItems.find((obj) => obj === item)._showDetails
|
||||
this.myItems.forEach((obj) => {
|
||||
if (obj === item) {
|
||||
obj._showDetails = !status
|
||||
} else {
|
||||
obj._showDetails = false
|
||||
}
|
||||
})
|
||||
await this.$nextTick()
|
||||
if (!status && this.$refs.rowDetails) {
|
||||
this.$refs.rowDetails.focus()
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
myItems() {
|
||||
return this.items.map((item) => {
|
||||
return { ...item, _showDetails: false }
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
35
admin/src/components/Tables/SelectUsersTable.vue
Normal file
35
admin/src/components/Tables/SelectUsersTable.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="component-select-users-table">
|
||||
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
||||
<template #cell(bookmark)="row">
|
||||
<div>
|
||||
<b-button
|
||||
v-if="row.item.emailChecked"
|
||||
variant="warning"
|
||||
size="md"
|
||||
@click="$emit('push-item', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="plus" variant="success"></b-icon>
|
||||
</b-button>
|
||||
<div v-else>{{ $t('e_mail') }}!</div>
|
||||
</div>
|
||||
</template>
|
||||
</b-table-lite>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectUsersTable',
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
26
admin/src/components/Tables/SelectedUsersTable.vue
Normal file
26
admin/src/components/Tables/SelectedUsersTable.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="component-selected-users-table">
|
||||
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button variant="danger" size="md" @click="$emit('remove-item', row.item)" class="mr-2">
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table-lite>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'SelectedUsersTable',
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,289 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserTable from './UserTable.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn()
|
||||
apolloQueryMock.mockResolvedValue()
|
||||
|
||||
describe('UserTable', () => {
|
||||
let wrapper
|
||||
|
||||
const defaultItemsUser = [
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
creation: [1000, 1000, 1000],
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
creation: [1000, 1000, 1000],
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
creation: [1000, 1000, 1000],
|
||||
},
|
||||
]
|
||||
|
||||
const confirmationItemsUser = [
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
amount: 10,
|
||||
memo: 'Test 1',
|
||||
date: '11-09-2001',
|
||||
moderator: 1,
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
amount: 10,
|
||||
memo: 'Test 2',
|
||||
date: '21-09-2001',
|
||||
moderator: 1,
|
||||
},
|
||||
{
|
||||
email: 'bibi@bloxberg.de',
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
amount: 10,
|
||||
memo: 'Test 3',
|
||||
date: '30-09-2001',
|
||||
moderator: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const propsDataPageUserSearch = {
|
||||
type: 'PageUserSearch',
|
||||
itemsUser: defaultItemsUser,
|
||||
fieldsTable: [
|
||||
'email',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'creation',
|
||||
'show_details',
|
||||
'confirm_mail',
|
||||
'transactions_list',
|
||||
],
|
||||
}
|
||||
|
||||
const propsDataUserListSearch = {
|
||||
type: 'UserListSearch',
|
||||
itemsUser: defaultItemsUser,
|
||||
fieldsTable: ['bookmark', 'email', 'firstName', 'lastName', 'creation'],
|
||||
creation: [1000, 1000, 1000],
|
||||
}
|
||||
|
||||
const propsDataUserListMassCreation = {
|
||||
type: 'UserListMassCreation',
|
||||
itemsUser: defaultItemsUser,
|
||||
fieldsTable: ['email', 'firstName', 'lastName', 'creation', 'bookmark'],
|
||||
creation: [1000, 1000, 1000],
|
||||
}
|
||||
|
||||
const propsDataPageCreationConfirm = {
|
||||
type: 'PageCreationConfirm',
|
||||
itemsUser: confirmationItemsUser,
|
||||
fieldsTable: [
|
||||
'bookmark',
|
||||
'email',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'amount',
|
||||
'memo',
|
||||
'date',
|
||||
'moderator',
|
||||
'edit_creation',
|
||||
'confirm',
|
||||
],
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$store: {
|
||||
commit: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = (propsData) => {
|
||||
return mount(UserTable, { localVue, propsData, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
describe('type PageUserSearch', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(propsDataPageUserSearch)
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-user-table', () => {
|
||||
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has a DIV element with the id overlay that is not displayed', () => {
|
||||
expect(wrapper.find('#overlay').exists()).toBeTruthy()
|
||||
expect(wrapper.find('#overlay').attributes('style')).toBe('display: none;')
|
||||
})
|
||||
|
||||
describe('table', () => {
|
||||
it('has a table', () => {
|
||||
expect(wrapper.find('table').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('header definition', () => {
|
||||
it('has 4 column', () => {
|
||||
expect(wrapper.findAll('th').length).toBe(7)
|
||||
})
|
||||
|
||||
it('has Email as first column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="1"] div').text()).toBe('Email')
|
||||
})
|
||||
|
||||
it('has First Name as second column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="2"] div').text()).toBe('First Name')
|
||||
})
|
||||
|
||||
it('has Last Name as third column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="3"] div').text()).toBe('Last Name')
|
||||
})
|
||||
|
||||
it('has Creation as fourth column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="4"] div').text()).toBe('Creation')
|
||||
})
|
||||
|
||||
it('has Creation as fifth column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="5"] div').text()).toBe('Show Details')
|
||||
})
|
||||
|
||||
it('has Creation as sixth column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="6"] div').text()).toBe('Confirm Mail')
|
||||
})
|
||||
|
||||
it('has Creation as seventh column', () => {
|
||||
expect(wrapper.find('th[aria-colindex="7"] div').text()).toBe('Transactions List')
|
||||
})
|
||||
})
|
||||
|
||||
describe('content', () => {
|
||||
it('has 3 rows', () => {
|
||||
expect(wrapper.findAll('tbody tr').length).toBe(3)
|
||||
})
|
||||
|
||||
it('has 7 columns', () => {
|
||||
expect(wrapper.findAll('tr:nth-child(1) > td').length).toBe(7)
|
||||
})
|
||||
|
||||
it('click button on fifth column', () => {
|
||||
wrapper.find('tbody tr td[aria-colindex="5"] button').trigger('click')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// it('expect(wrapper.html()).', () => {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log(wrapper.html())
|
||||
// })
|
||||
})
|
||||
|
||||
describe('type UserListSearch', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(propsDataUserListSearch)
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-user-table', () => {
|
||||
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
// it('expect(wrapper.html()).', () => {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log(wrapper.html())
|
||||
// })
|
||||
})
|
||||
|
||||
describe('type UserListMassCreation', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(propsDataUserListMassCreation)
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-user-table', () => {
|
||||
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
// it('expect(wrapper.html()).', () => {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log(wrapper.html())
|
||||
// })
|
||||
})
|
||||
|
||||
describe('type PageCreationConfirm', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper(propsDataPageCreationConfirm)
|
||||
})
|
||||
|
||||
it('has a DIV element with the class.component-user-table', () => {
|
||||
expect(wrapper.find('.component-user-table').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
// it('expect(wrapper.html()).', () => {
|
||||
// // eslint-disable-next-line no-console
|
||||
// console.log(wrapper.html())
|
||||
// })
|
||||
})
|
||||
/**
|
||||
<user-table
|
||||
v-if="itemsList.length > 0"
|
||||
type="UserListSearch"
|
||||
:itemsUser="itemsList"
|
||||
:fieldsTable="Searchfields"
|
||||
:criteria="criteria"
|
||||
:creation="creation"
|
||||
@update-item="updateItem"
|
||||
/>
|
||||
|
||||
<user-table
|
||||
v-show="itemsMassCreation.length > 0"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
type="UserListMassCreation"
|
||||
:itemsUser="itemsMassCreation"
|
||||
:fieldsTable="fields"
|
||||
:criteria="null"
|
||||
:creation="creation"
|
||||
@update-item="updateItem"
|
||||
/>
|
||||
|
||||
<user-table
|
||||
class="mt-4"
|
||||
type="PageCreationConfirm"
|
||||
:itemsUser="confirmResult"
|
||||
:fieldsTable="fields"
|
||||
@remove-confirm-result="removeConfirmResult"
|
||||
/>
|
||||
|
||||
<user-table
|
||||
type="PageUserSearch"
|
||||
:itemsUser="searchResult"
|
||||
:fieldsTable="fields"
|
||||
:criteria="criteria"
|
||||
/>
|
||||
*/
|
||||
})
|
||||
})
|
||||
@ -1,344 +0,0 @@
|
||||
<template>
|
||||
<div class="component-user-table">
|
||||
<div v-show="overlay" id="overlay" class="">
|
||||
<b-jumbotron class="bg-light p-4">
|
||||
<template #header>{{ overlayText.header }}</template>
|
||||
|
||||
<template #lead>
|
||||
{{ overlayText.text1 }}
|
||||
</template>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<p>
|
||||
{{ overlayText.text2 }}
|
||||
</p>
|
||||
|
||||
<b-button size="md" variant="danger" class="m-3" @click="overlayCancel">
|
||||
{{ overlayText.button_cancel }}
|
||||
</b-button>
|
||||
<b-button
|
||||
size="md"
|
||||
variant="success"
|
||||
class="m-3 text-right"
|
||||
@click="overlayOK(overlayBookmarkType, overlayItem)"
|
||||
>
|
||||
{{ overlayText.button_ok }}
|
||||
</b-button>
|
||||
</b-jumbotron>
|
||||
</div>
|
||||
<b-table-lite
|
||||
:items="itemsUser"
|
||||
:fields="fieldsTable"
|
||||
:filter="criteria"
|
||||
caption-top
|
||||
striped
|
||||
hover
|
||||
stacked="md"
|
||||
>
|
||||
<template #cell(creation)="data">
|
||||
<div v-html="data.value"></div>
|
||||
</template>
|
||||
|
||||
<template #cell(edit_creation)="row">
|
||||
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
|
||||
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(show_details)="row">
|
||||
<b-button variant="info" size="md" @click="rowToogleDetails(row, 0)" class="mr-2">
|
||||
<b-icon :icon="row.detailsShowing ? 'eye-slash-fill' : 'eye-fill'"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(confirm_mail)="row">
|
||||
<b-button
|
||||
:variant="row.item.emailChecked ? 'success' : 'danger'"
|
||||
size="md"
|
||||
@click="rowToogleDetails(row, 1)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon
|
||||
:icon="row.item.emailChecked ? 'envelope-open' : 'envelope'"
|
||||
aria-label="Help"
|
||||
></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(transactions_list)="row">
|
||||
<b-button variant="warning" size="md" @click="rowToogleDetails(row, 2)" class="mr-2">
|
||||
<b-icon icon="list"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #row-details="row">
|
||||
<row-details
|
||||
v-if="type !== 'UserListSearch' && type !== 'UserListMassCreation'"
|
||||
:row="row"
|
||||
:type="type"
|
||||
:slotName="slotName"
|
||||
:index="slotIndex"
|
||||
@row-toogle-details="rowToogleDetails"
|
||||
>
|
||||
<template #show-creation>
|
||||
<div>
|
||||
<creation-formular
|
||||
v-if="type === 'PageUserSearch'"
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
<edit-creation-formular
|
||||
v-else
|
||||
type="singleCreation"
|
||||
:pagetype="type"
|
||||
:creation="row.item.creation"
|
||||
:item="row.item"
|
||||
:row="row"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #show-register-mail>
|
||||
<confirm-register-mail-formular
|
||||
:checked="row.item.emailChecked"
|
||||
:email="row.item.email"
|
||||
:dateLastSend="$moment().subtract(1, 'month').format('dddd, DD.MMMM.YYYY HH:mm'),"
|
||||
/>
|
||||
</template>
|
||||
<template #show-transaction-list>
|
||||
<creation-transaction-list-formular :userId="row.item.userId" />
|
||||
</template>
|
||||
</row-details>
|
||||
</template>
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button
|
||||
variant="warning"
|
||||
v-show="type === 'UserListSearch'"
|
||||
size="md"
|
||||
@click="bookmarkPush(row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="plus" variant="success"></b-icon>
|
||||
</b-button>
|
||||
<b-button
|
||||
variant="danger"
|
||||
v-show="type === 'UserListMassCreation' || type === 'PageCreationConfirm'"
|
||||
size="md"
|
||||
@click="overlayShow('remove', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="x" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<template #cell(confirm)="row">
|
||||
<b-button
|
||||
variant="success"
|
||||
v-show="type === 'PageCreationConfirm'"
|
||||
size="md"
|
||||
@click="overlayShow('confirm', row.item)"
|
||||
class="mr-2"
|
||||
>
|
||||
<b-icon icon="check" scale="2" variant=""></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table-lite>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CreationFormular from '../components/CreationFormular.vue'
|
||||
import EditCreationFormular from '../components/EditCreationFormular.vue'
|
||||
import ConfirmRegisterMailFormular from '../components/ConfirmRegisterMailFormular.vue'
|
||||
import CreationTransactionListFormular from '../components/CreationTransactionListFormular.vue'
|
||||
import RowDetails from '../components/RowDetails.vue'
|
||||
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
|
||||
const slotNames = ['show-creation', 'show-register-mail', 'show-transaction-list']
|
||||
|
||||
export default {
|
||||
name: 'UserTable',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
itemsUser: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fieldsTable: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
criteria: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
creation: {
|
||||
type: Array,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CreationFormular,
|
||||
EditCreationFormular,
|
||||
ConfirmRegisterMailFormular,
|
||||
CreationTransactionListFormular,
|
||||
RowDetails,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCreationFormular: null,
|
||||
showConfirmRegisterMailFormular: null,
|
||||
showCreationTransactionListFormular: null,
|
||||
creationUserData: {},
|
||||
overlay: false,
|
||||
overlayBookmarkType: '',
|
||||
overlayItem: [],
|
||||
overlayText: [
|
||||
{
|
||||
header: '-',
|
||||
text1: '--',
|
||||
text2: '---',
|
||||
button_ok: 'OK',
|
||||
button_cancel: 'Cancel',
|
||||
},
|
||||
],
|
||||
slotIndex: 0,
|
||||
openRow: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowToogleDetails(row, index) {
|
||||
if (this.openRow) {
|
||||
if (this.openRow.index === row.index) {
|
||||
if (index === this.slotIndex) {
|
||||
row.toggleDetails()
|
||||
this.openRow = null
|
||||
} else {
|
||||
this.slotIndex = index
|
||||
}
|
||||
} else {
|
||||
this.openRow.toggleDetails()
|
||||
row.toggleDetails()
|
||||
this.slotIndex = index
|
||||
this.openRow = row
|
||||
if (this.type === 'PageCreationConfirm') {
|
||||
this.creationUserData = row.item
|
||||
}
|
||||
}
|
||||
} else {
|
||||
row.toggleDetails()
|
||||
this.slotIndex = index
|
||||
this.openRow = row
|
||||
if (this.type === 'PageCreationConfirm') {
|
||||
this.creationUserData = row.item
|
||||
}
|
||||
}
|
||||
},
|
||||
overlayShow(bookmarkType, item) {
|
||||
this.overlay = true
|
||||
this.overlayBookmarkType = bookmarkType
|
||||
this.overlayItem = item
|
||||
|
||||
if (bookmarkType === 'remove') {
|
||||
this.overlayText.header = this.$t('overlay.remove.title')
|
||||
this.overlayText.text1 = this.$t('overlay.remove.text')
|
||||
this.overlayText.text2 = this.$t('overlay.remove.question')
|
||||
this.overlayText.button_ok = this.$t('overlay.remove.yes')
|
||||
this.overlayText.button_cancel = this.$t('overlay.remove.no')
|
||||
}
|
||||
if (bookmarkType === 'confirm') {
|
||||
this.overlayText.header = this.$t('overlay.confirm.title')
|
||||
this.overlayText.text1 = this.$t('overlay.confirm.text')
|
||||
this.overlayText.text2 = this.$t('overlay.confirm.question')
|
||||
this.overlayText.button_ok = this.$t('overlay.confirm.yes')
|
||||
this.overlayText.button_cancel = this.$t('overlay.confirm.no')
|
||||
}
|
||||
},
|
||||
overlayOK(bookmarkType, item) {
|
||||
if (bookmarkType === 'remove') {
|
||||
this.bookmarkRemove(item)
|
||||
}
|
||||
if (bookmarkType === 'confirm') {
|
||||
this.bookmarkConfirm(item)
|
||||
}
|
||||
this.overlay = false
|
||||
},
|
||||
overlayCancel() {
|
||||
this.overlay = false
|
||||
},
|
||||
bookmarkPush(item) {
|
||||
this.$emit('update-item', item, 'push')
|
||||
},
|
||||
bookmarkRemove(item) {
|
||||
if (this.type === 'UserListMassCreation') {
|
||||
this.$emit('update-item', item, 'remove')
|
||||
}
|
||||
|
||||
if (this.type === 'PageCreationConfirm') {
|
||||
this.$emit('remove-confirm-result', item, 'remove')
|
||||
}
|
||||
},
|
||||
bookmarkConfirm(item) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: confirmPendingCreation,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.$emit('remove-confirm-result', item, 'confirmed')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
},
|
||||
updateCreationData(data) {
|
||||
this.creationUserData.amount = data.amount
|
||||
this.creationUserData.date = data.date
|
||||
this.creationUserData.memo = data.memo
|
||||
this.creationUserData.moderator = data.moderator
|
||||
|
||||
data.row.toggleDetails()
|
||||
},
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
slotName() {
|
||||
return slotNames[this.slotIndex]
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-left: 5%;
|
||||
background-color: rgba(12, 11, 11, 0.781);
|
||||
z-index: 1000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
7
admin/src/graphql/deleteUser.js
Normal file
7
admin/src/graphql/deleteUser.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const deleteUser = gql`
|
||||
mutation ($userId: Float!) {
|
||||
deleteUser(userId: $userId)
|
||||
}
|
||||
`
|
||||
@ -1,12 +1,19 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const searchUsers = gql`
|
||||
query ($searchText: String!, $currentPage: Int, $pageSize: Int, $notActivated: Boolean) {
|
||||
query (
|
||||
$searchText: String!
|
||||
$currentPage: Int
|
||||
$pageSize: Int
|
||||
$notActivated: Boolean
|
||||
$isDeleted: Boolean
|
||||
) {
|
||||
searchUsers(
|
||||
searchText: $searchText
|
||||
currentPage: $currentPage
|
||||
pageSize: $pageSize
|
||||
notActivated: $notActivated
|
||||
isDeleted: $isDeleted
|
||||
) {
|
||||
userCount
|
||||
userList {
|
||||
@ -16,6 +23,9 @@ export const searchUsers = gql`
|
||||
email
|
||||
creation
|
||||
emailChecked
|
||||
hasElopage
|
||||
emailConfirmationSend
|
||||
deletedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
admin/src/graphql/unDeleteUser.js
Normal file
7
admin/src/graphql/unDeleteUser.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const unDeleteUser = gql`
|
||||
mutation ($userId: Float!) {
|
||||
unDeleteUser(userId: $userId)
|
||||
}
|
||||
`
|
||||
@ -54,12 +54,21 @@ const dateTimeFormats = {
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
monthShort: {
|
||||
month: 'short',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
},
|
||||
de: {
|
||||
short: {
|
||||
@ -69,12 +78,21 @@ const dateTimeFormats = {
|
||||
},
|
||||
long: {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
weekday: 'short',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
monthShort: {
|
||||
month: 'short',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
{
|
||||
"all_emails": "Alle Nutzer",
|
||||
"back": "zurück",
|
||||
"bookmark": "bookmark",
|
||||
"confirmed": "bestätigt",
|
||||
"creation": "Schöpfung",
|
||||
"creation_form": {
|
||||
"creation_for": "Schöpfung für",
|
||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||
"creation_for": "Aktives Grundeinkommen für",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
"min_characters": "Mindestens 10 Zeichen eingeben",
|
||||
@ -12,16 +15,29 @@
|
||||
"select_value": "Betrag auswählen",
|
||||
"submit_creation": "Schöpfung einreichen",
|
||||
"toasted": "Offene Schöpfung ({value} GDD) für {email} wurde gespeichert und liegt zur Bestätigung bereit",
|
||||
"toasted_created": "Schöpfung wurde erfolgreich gespeichert",
|
||||
"toasted_default": "`Fall {event} wird nicht unterstützt`",
|
||||
"toasted_delete": "Offene Schöpfung wurde gelöscht",
|
||||
"toasted_update": "`Offene Schöpfung {value} GDD) für {email} wurde geändert und liegt zur Bestätigung bereit",
|
||||
"update_creation": "Schöpfung aktualisieren"
|
||||
},
|
||||
"creation_for_month": "Schöpfung für Monat",
|
||||
"date": "Datum",
|
||||
"delete": "Löschen",
|
||||
"deleted": "gelöscht",
|
||||
"deleted_user": "Alle gelöschten Nutzer",
|
||||
"delete_user": "Nutzer löschen",
|
||||
"details": "Details",
|
||||
"edit": "Bearbeiten",
|
||||
"error": "Fehler",
|
||||
"e_mail": "E-Mail",
|
||||
"firstname": "Vorname",
|
||||
"gradido_admin_footer": "Gradido Akademie Adminkonsole",
|
||||
"hide_details": "Details verbergen von",
|
||||
"hide_details": "Details verbergen",
|
||||
"lastname": "Nachname",
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
"name": "Name",
|
||||
"navbar": {
|
||||
"logout": "Abmelden",
|
||||
"multi_creation": "Mehrfachschöpfung",
|
||||
@ -35,8 +51,8 @@
|
||||
"open_creations": "Offene Schöpfungen",
|
||||
"overlay": {
|
||||
"confirm": {
|
||||
"no": "Nein, nicht speichern.",
|
||||
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und entgültig speichern?",
|
||||
"cancel": "Abbrechen",
|
||||
"question": "Willst du diese vorgespeicherte Schöpfung wirklich vollziehen und endgültig speichern?",
|
||||
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
|
||||
"title": "Schöpfung bestätigen!",
|
||||
"yes": "Ja, Schöpfung bestätigen und speichern!"
|
||||
@ -50,18 +66,34 @@
|
||||
}
|
||||
},
|
||||
"remove": "Entfernen",
|
||||
"removeNotSelf": "Als Admin / Moderator kannst du dich nicht selber löschen.",
|
||||
"remove_all": "alle Nutzer entfernen",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
"success": "Erfolg",
|
||||
"text": "Text",
|
||||
"transaction": "Transaktion",
|
||||
"transactionlist": {
|
||||
"amount": "Betrag",
|
||||
"community": "Gemeinschaft",
|
||||
"date": "Datum",
|
||||
"decay": "Vergänglichkeit",
|
||||
"memo": "Nachricht",
|
||||
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
||||
},
|
||||
"undelete_user": "Nutzer wiederherstellen",
|
||||
"unregistered_emails": "Nur unregistrierte Nutzer",
|
||||
"unregister_mail": {
|
||||
"button": "Registrierungs-Email bestätigen, jetzt senden",
|
||||
"error": "Fehler beim Senden des Bestätigungs-Links an den Benutzer: {message}",
|
||||
"info": "Email bestätigen, wiederholt senden an:",
|
||||
"never_sent": "Es scheint so, als ob wir nie eine E-Mail an {email} geschickt haben",
|
||||
"success": "Erfolgreiches Senden des Bestätigungs-Links an die E-Mail des Nutzers! ({email})",
|
||||
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({mail}) gesendet.",
|
||||
"text_true": " Die Email wurde am {date} Uhr bestätigt."
|
||||
"text_false": " Die letzte Email wurde am {date} Uhr an das Mitglied ({email}) gesendet.",
|
||||
"text_true": " Die Email wurde bestätigt."
|
||||
},
|
||||
"userIsDeleted": "Der Nutzer ist gelöscht. Es können keine GDD mehr geschöpft werden.",
|
||||
"user_deleted": "Nutzer ist gelöscht.",
|
||||
"user_recovered": "Nutzer ist wiederhergestellt.",
|
||||
"user_search": "Nutzer-Suche"
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
{
|
||||
"all_emails": "All users",
|
||||
"back": "back",
|
||||
"bookmark": "Remember",
|
||||
"confirmed": "confirmed",
|
||||
"creation": "Creation",
|
||||
"creation_form": {
|
||||
"creation_for": "Creation for",
|
||||
"creation_failed": "Could not create pending creation for {email}",
|
||||
"creation_for": "Active Basic Income for",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
"min_characters": "Enter at least 10 characters",
|
||||
@ -12,16 +15,29 @@
|
||||
"select_value": "Select amount",
|
||||
"submit_creation": "Submit creation",
|
||||
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
|
||||
"toasted_created": "Creation has been successfully saved",
|
||||
"toasted_default": "`Case {event} is not supported`",
|
||||
"toasted_delete": "Open creation has been deleted",
|
||||
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
|
||||
"update_creation": "Creation update"
|
||||
},
|
||||
"creation_for_month": "Creation for month",
|
||||
"date": "Date",
|
||||
"delete": "Delete",
|
||||
"deleted": "deleted",
|
||||
"deleted_user": "All deleted user",
|
||||
"delete_user": "Delete user",
|
||||
"details": "Details",
|
||||
"e_mail": "E-Mail",
|
||||
"edit": "Edit",
|
||||
"error": "Error",
|
||||
"e_mail": "E-mail",
|
||||
"firstname": "Firstname",
|
||||
"gradido_admin_footer": "Gradido Academy Admin Console",
|
||||
"hide_details": "Hide details from",
|
||||
"hide_details": "Hide details",
|
||||
"lastname": "Lastname",
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
"name": "Name",
|
||||
"navbar": {
|
||||
"logout": "Logout",
|
||||
"multi_creation": "Multiple creation",
|
||||
@ -35,7 +51,7 @@
|
||||
"open_creations": "Open creations",
|
||||
"overlay": {
|
||||
"confirm": {
|
||||
"no": "No, do not save.",
|
||||
"cancel": "Cancel",
|
||||
"question": "Do you really want to carry out and finally save this pre-stored creation?",
|
||||
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
|
||||
"title": "Confirm creation!",
|
||||
@ -50,18 +66,34 @@
|
||||
}
|
||||
},
|
||||
"remove": "Remove",
|
||||
"removeNotSelf": "As admin / moderator you cannot delete yourself.",
|
||||
"remove_all": "Remove all users",
|
||||
"save": "Speichern",
|
||||
"status": "Status",
|
||||
"success": "Success",
|
||||
"text": "Text",
|
||||
"transaction": "Transaction",
|
||||
"transactionlist": {
|
||||
"amount": "Amount",
|
||||
"community": "Community",
|
||||
"date": "Date",
|
||||
"decay": "Decay",
|
||||
"memo": "Message",
|
||||
"title": "All creation-transactions for the user"
|
||||
},
|
||||
"undelete_user": "Undelete User",
|
||||
"unregistered_emails": "Only unregistered users",
|
||||
"unregister_mail": {
|
||||
"button": "Confirm registration email, send now",
|
||||
"error": "Error sending the confirmation link to the user: {message}",
|
||||
"info": "Confirm email, send repeatedly to:",
|
||||
"never_sent": "It seems we did never send an email to the member {email}",
|
||||
"success": "Successfully send the confirmation link to the user's email! ({email})",
|
||||
"text_false": "The last email was sent to the member ({mail}) on {date} clock.",
|
||||
"text_true": "The email was confirmed on {date} clock."
|
||||
"text_false": "The last email was sent to the member ({email}) on {date}.",
|
||||
"text_true": "The email was confirmed."
|
||||
},
|
||||
"userIsDeleted": "The user is deleted. No more GDD can be created.",
|
||||
"user_deleted": "User is deleted.",
|
||||
"user_recovered": "User is recovered.",
|
||||
"user_search": "User search"
|
||||
}
|
||||
|
||||
@ -13,39 +13,31 @@ import i18n from './i18n'
|
||||
|
||||
import VueApollo from 'vue-apollo'
|
||||
|
||||
import PortalVue from 'portal-vue'
|
||||
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
import 'bootstrap/dist/css/bootstrap.css'
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
|
||||
import moment from 'vue-moment'
|
||||
import Toasted from 'vue-toasted'
|
||||
import { toasters } from './mixins/toaster'
|
||||
|
||||
import { apolloProvider } from './plugins/apolloProvider'
|
||||
|
||||
Vue.use(PortalVue)
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
Vue.use(IconsPlugin)
|
||||
|
||||
Vue.use(moment)
|
||||
|
||||
Vue.use(VueApollo)
|
||||
|
||||
Vue.use(Toasted, {
|
||||
position: 'top-center',
|
||||
duration: 5000,
|
||||
fullWidth: true,
|
||||
action: {
|
||||
text: 'x',
|
||||
onClick: (e, toastObject) => {
|
||||
toastObject.goAway(0)
|
||||
},
|
||||
},
|
||||
})
|
||||
Vue.mixin(toasters)
|
||||
|
||||
addNavigationGuards(router, store, apolloProvider.defaultClient, i18n)
|
||||
|
||||
i18n.locale =
|
||||
store.state.moderator && store.state.moderator.language ? store.state.moderator.language : 'en'
|
||||
|
||||
new Vue({
|
||||
moment,
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
|
||||
@ -6,7 +6,6 @@ import Vue from 'vue'
|
||||
import VueApollo from 'vue-apollo'
|
||||
import i18n from './i18n'
|
||||
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
import moment from 'vue-moment'
|
||||
import store from './store/store'
|
||||
import router from './router/router'
|
||||
|
||||
@ -14,8 +13,15 @@ jest.mock('vue')
|
||||
jest.mock('vue-apollo')
|
||||
jest.mock('vuex')
|
||||
jest.mock('vue-i18n')
|
||||
jest.mock('vue-moment')
|
||||
jest.mock('./store/store')
|
||||
jest.mock('./store/store', () => {
|
||||
return {
|
||||
state: {
|
||||
moderator: {
|
||||
language: 'es',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
jest.mock('./i18n')
|
||||
jest.mock('./router/router')
|
||||
|
||||
@ -82,10 +88,6 @@ describe('main', () => {
|
||||
expect(Vue.use).toBeCalledWith(IconsPlugin)
|
||||
})
|
||||
|
||||
it('calls Moment', () => {
|
||||
expect(Vue.use).toBeCalledWith(moment)
|
||||
})
|
||||
|
||||
it('creates a store', () => {
|
||||
expect(Vue).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -101,4 +103,8 @@ describe('main', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('sets the locale from store', () => {
|
||||
expect(i18n.locale).toBe('es')
|
||||
})
|
||||
})
|
||||
|
||||
41
admin/src/mixins/creationMonths.js
Normal file
41
admin/src/mixins/creationMonths.js
Normal file
@ -0,0 +1,41 @@
|
||||
export const creationMonths = {
|
||||
props: {
|
||||
creation: {
|
||||
type: Array,
|
||||
default: () => [1000, 1000, 1000],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
creationDates() {
|
||||
const now = new Date(Date.now())
|
||||
const dates = [now]
|
||||
for (let i = 1; i < 3; i++) {
|
||||
dates.push(new Date(now.getFullYear(), now.getMonth() - i, 1))
|
||||
}
|
||||
return dates.reverse()
|
||||
},
|
||||
creationDateObjects() {
|
||||
const result = []
|
||||
this.creationDates.forEach((date) => {
|
||||
result.push({
|
||||
short: this.$d(date, 'month'),
|
||||
long: this.$d(date, 'short'),
|
||||
year: this.$d(date, 'year'),
|
||||
date: this.$d(date, 'short', 'en'),
|
||||
})
|
||||
})
|
||||
return result
|
||||
},
|
||||
radioOptions() {
|
||||
return this.creationDateObjects.map((obj, idx) => {
|
||||
return {
|
||||
item: { ...obj, creation: this.creation[idx] },
|
||||
name: obj.short + (this.creation[idx] ? ' ' + this.creation[idx] + ' GDD' : ''),
|
||||
}
|
||||
})
|
||||
},
|
||||
creationLabel() {
|
||||
return this.creationDates.map((date) => this.$d(date, 'monthShort')).join(' | ')
|
||||
},
|
||||
},
|
||||
}
|
||||
30
admin/src/mixins/toaster.js
Normal file
30
admin/src/mixins/toaster.js
Normal file
@ -0,0 +1,30 @@
|
||||
export const toasters = {
|
||||
methods: {
|
||||
toastSuccess(message) {
|
||||
this.toast(message, {
|
||||
title: this.$t('success'),
|
||||
variant: 'success',
|
||||
})
|
||||
},
|
||||
toastError(message) {
|
||||
this.toast(message, {
|
||||
title: this.$t('error'),
|
||||
variant: 'danger',
|
||||
})
|
||||
},
|
||||
toast(message, options) {
|
||||
// for unit tests, check that replace is present
|
||||
if (message.replace) message = message.replace(/^GraphQL error: /, '')
|
||||
this.$bvToast.toast(message, {
|
||||
autoHideDelay: 5000,
|
||||
appendToast: true,
|
||||
solid: true,
|
||||
toaster: 'b-toaster-top-right',
|
||||
headerClass: 'gdd-toaster-title',
|
||||
bodyClass: 'gdd-toaster-body',
|
||||
toastClass: 'gdd-toaster',
|
||||
...options,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
34
admin/src/mixins/toggleRowDetails.js
Normal file
34
admin/src/mixins/toggleRowDetails.js
Normal file
@ -0,0 +1,34 @@
|
||||
export const toggleRowDetails = {
|
||||
data() {
|
||||
return {
|
||||
slotIndex: 0,
|
||||
openRow: null,
|
||||
creationUserData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
rowToggleDetails(row, index) {
|
||||
if (this.openRow) {
|
||||
if (this.openRow.index === row.index) {
|
||||
if (index === this.slotIndex) {
|
||||
row.toggleDetails()
|
||||
this.openRow = null
|
||||
} else {
|
||||
this.slotIndex = index
|
||||
}
|
||||
} else {
|
||||
this.openRow.toggleDetails()
|
||||
row.toggleDetails()
|
||||
this.slotIndex = index
|
||||
this.openRow = row
|
||||
this.creationUserData = row.item
|
||||
}
|
||||
} else {
|
||||
row.toggleDetails()
|
||||
this.slotIndex = index
|
||||
this.openRow = row
|
||||
this.creationUserData = row.item
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
141
admin/src/mixins/toggleRowDetails.test.js
Normal file
141
admin/src/mixins/toggleRowDetails.test.js
Normal file
@ -0,0 +1,141 @@
|
||||
import { toggleRowDetails } from './toggleRowDetails'
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const Component = {
|
||||
render() {},
|
||||
mixins: [toggleRowDetails],
|
||||
}
|
||||
|
||||
const toggleDetailsMock = jest.fn()
|
||||
const secondToggleDetailsMock = jest.fn()
|
||||
|
||||
const row = {
|
||||
toggleDetails: toggleDetailsMock,
|
||||
index: 0,
|
||||
item: {
|
||||
data: 'item-data',
|
||||
},
|
||||
}
|
||||
|
||||
let wrapper
|
||||
|
||||
describe('toggleRowDetails', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = mount(Component, { localVue })
|
||||
})
|
||||
|
||||
it('sets default data', () => {
|
||||
expect(wrapper.vm.slotIndex).toBe(0)
|
||||
expect(wrapper.vm.openRow).toBe(null)
|
||||
expect(wrapper.vm.creationUserData).toEqual({})
|
||||
})
|
||||
|
||||
describe('no open row', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.rowToggleDetails(row, 2)
|
||||
})
|
||||
|
||||
it('calls toggleDetails', () => {
|
||||
expect(toggleDetailsMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('updates slot index', () => {
|
||||
expect(wrapper.vm.slotIndex).toBe(2)
|
||||
})
|
||||
|
||||
it('updates open row', () => {
|
||||
expect(wrapper.vm.openRow).toEqual(
|
||||
expect.objectContaining({
|
||||
index: 0,
|
||||
item: {
|
||||
data: 'item-data',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('updates creation user data', () => {
|
||||
expect(wrapper.vm.creationUserData).toEqual({ data: 'item-data' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('with open row', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setData({ openRow: row })
|
||||
})
|
||||
|
||||
describe('row index is open row index', () => {
|
||||
describe('index is slot index', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.rowToggleDetails(row, 0)
|
||||
})
|
||||
|
||||
it('calls toggleDetails', () => {
|
||||
expect(toggleDetailsMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('sets open row to null', () => {
|
||||
expect(wrapper.vm.openRow).toBe(null)
|
||||
})
|
||||
})
|
||||
|
||||
describe('index is not slot index', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.rowToggleDetails(row, 2)
|
||||
})
|
||||
|
||||
it('does not call toggleDetails', () => {
|
||||
expect(toggleDetailsMock).not.toBeCalled()
|
||||
})
|
||||
|
||||
it('updates slot index', () => {
|
||||
expect(wrapper.vm.slotIndex).toBe(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('row index is not open row index', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.vm.rowToggleDetails(
|
||||
{
|
||||
toggleDetails: secondToggleDetailsMock,
|
||||
index: 2,
|
||||
item: {
|
||||
data: 'new-item-data',
|
||||
},
|
||||
},
|
||||
2,
|
||||
)
|
||||
})
|
||||
|
||||
it('closes the open row', () => {
|
||||
expect(toggleDetailsMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('opens the new row', () => {
|
||||
expect(secondToggleDetailsMock).toBeCalled()
|
||||
})
|
||||
|
||||
it('updates slot index', () => {
|
||||
expect(wrapper.vm.slotIndex).toBe(2)
|
||||
})
|
||||
|
||||
it('updates open row', () => {
|
||||
expect(wrapper.vm.openRow).toEqual({
|
||||
toggleDetails: secondToggleDetailsMock,
|
||||
index: 2,
|
||||
item: {
|
||||
data: 'new-item-data',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('updates creation user data', () => {
|
||||
expect(wrapper.vm.creationUserData).toEqual({ data: 'new-item-data' })
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Creation from './Creation.vue'
|
||||
import Vue from 'vue'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -15,6 +15,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
emailChecked: true,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
@ -22,43 +23,39 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
emailChecked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$t: jest.fn((t, options) => (options ? [t, options] : t)),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
state: {
|
||||
userSelectedInMassCreation: [],
|
||||
},
|
||||
},
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
describe('Creation', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return shallowMount(Creation, { localVue, mocks })
|
||||
return mount(Creation, { localVue, mocks })
|
||||
}
|
||||
|
||||
describe('shallowMount', () => {
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
@ -68,19 +65,81 @@ describe('Creation', () => {
|
||||
|
||||
describe('apollo returns user array', () => {
|
||||
it('calls the searchUser query', () => {
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('sets the data of itemsList', () => {
|
||||
expect(wrapper.vm.itemsList).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
showDetails: false,
|
||||
},
|
||||
it('has two rows in the left table', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('has nwo rows in the right table', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('has correct data in first row ', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi')
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'Bloxberg',
|
||||
)
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'200 | 400 | 600',
|
||||
)
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'bibi@bloxberg.de',
|
||||
)
|
||||
})
|
||||
|
||||
it('has correct data in second row ', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
|
||||
'Benjamin',
|
||||
)
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
|
||||
'Blümchen',
|
||||
)
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
|
||||
'800 | 600 | 400',
|
||||
)
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
|
||||
'benjamin@bluemchen.de',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('push item', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click')
|
||||
})
|
||||
|
||||
it('has one item in left table', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('has one item in right table', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('has the correct user in left table', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'bibi@bloxberg.de',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the correct user in right table', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'benjamin@bluemchen.de',
|
||||
)
|
||||
})
|
||||
|
||||
it('updates userSelectedInMassCreation in store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
@ -88,126 +147,64 @@ describe('Creation', () => {
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
showDetails: false,
|
||||
emailChecked: true,
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('update item', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
describe('remove item', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findAll('table')
|
||||
.at(1)
|
||||
.findAll('tbody > tr')
|
||||
.at(0)
|
||||
.find('button')
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
describe('push', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.findComponent({ name: 'UserTable' }).vm.$emit(
|
||||
'update-item',
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
showDetails: false,
|
||||
},
|
||||
'push',
|
||||
it('has two items in left table', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('has the removed user in first row', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'benjamin@bluemchen.de',
|
||||
)
|
||||
})
|
||||
|
||||
it('removes the pushed item from itemsList', () => {
|
||||
expect(wrapper.vm.itemsList).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
showDetails: false,
|
||||
},
|
||||
])
|
||||
it('has no items in right table', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('adds the pushed item to itemsMassCreation', () => {
|
||||
expect(wrapper.vm.itemsMassCreation).toEqual([
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
showDetails: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
describe('remove', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.findComponent({ name: 'UserTable' }).vm.$emit(
|
||||
'update-item',
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
showDetails: false,
|
||||
},
|
||||
'remove',
|
||||
)
|
||||
})
|
||||
|
||||
it('removes the item from itemsMassCreation', () => {
|
||||
expect(wrapper.vm.itemsMassCreation).toEqual([])
|
||||
})
|
||||
|
||||
it('adds the item to itemsList', () => {
|
||||
expect(wrapper.vm.itemsList).toEqual([
|
||||
{
|
||||
userId: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
showDetails: false,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
showDetails: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
it('commits empty array as userSelectedInMassCreation', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
|
||||
})
|
||||
})
|
||||
|
||||
describe('error', () => {
|
||||
const consoleErrorMock = jest.fn()
|
||||
const warnHandler = Vue.config.warnHandler
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.config.warnHandler = (w) => {}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error = consoleErrorMock
|
||||
wrapper.findComponent({ name: 'UserTable' }).vm.$emit('update-item', {}, 'no-rule')
|
||||
describe('remove all bookmarks', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('button.btn-light').trigger('click')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Vue.config.warnHandler = warnHandler
|
||||
it('has no items in right table', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('throws an error', () => {
|
||||
expect(consoleErrorMock).toBeCalledWith(expect.objectContaining({ message: 'no-rule' }))
|
||||
it('commits empty array to userSelectedInMassCreation', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
|
||||
})
|
||||
|
||||
it('calls searchUsers', () => {
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove all bookmarks', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findComponent({ name: 'UserTable' }).vm.$emit(
|
||||
'update-item',
|
||||
describe('store has items in userSelectedInMassCreation', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$store.state.userSelectedInMassCreation = [
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
@ -215,18 +212,43 @@ describe('Creation', () => {
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [800, 600, 400],
|
||||
showDetails: false,
|
||||
emailChecked: true,
|
||||
},
|
||||
'push',
|
||||
]
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has one item in left table', () => {
|
||||
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('has one item in right table', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('has the stored user in second row', () => {
|
||||
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
|
||||
'benjamin@bluemchen.de',
|
||||
)
|
||||
wrapper.findComponent({ name: 'CreationFormular' }).vm.$emit('remove-all-bookmark')
|
||||
})
|
||||
})
|
||||
|
||||
describe('failed creations', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'CreationFormular' })
|
||||
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
|
||||
})
|
||||
|
||||
it('removes all items from itemsMassCreation', () => {
|
||||
expect(wrapper.vm.itemsMassCreation).toEqual([])
|
||||
})
|
||||
|
||||
it('adds all items to itemsList', () => {
|
||||
expect(wrapper.vm.itemsList).toHaveLength(2)
|
||||
it('toasts two error messages', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith([
|
||||
'creation_form.creation_failed',
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
])
|
||||
expect(toastErrorSpy).toBeCalledWith([
|
||||
'creation_form.creation_failed',
|
||||
{ email: 'benjamin@bluemchen.de' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@ -235,14 +257,51 @@ describe('Creation', () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it('calls API when criteria changes', async () => {
|
||||
await wrapper.setData({ criteria: 'XX' })
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
describe('search criteria', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({ criteria: 'XX' })
|
||||
})
|
||||
|
||||
it('calls API when criteria changes', async () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: 'XX',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('reset search criteria', () => {
|
||||
it('calls the API', async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('.test-click-clear-criteria').trigger('click')
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('calls API when currentPage changes', async () => {
|
||||
await wrapper.setData({ currentPage: 2 })
|
||||
expect(apolloQueryMock).toBeCalled()
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 2,
|
||||
pageSize: 25,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -255,7 +314,7 @@ describe('Creation', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,20 +3,25 @@
|
||||
<b-row>
|
||||
<b-col cols="12" lg="6">
|
||||
<label>Usersuche</label>
|
||||
<b-input
|
||||
type="text"
|
||||
v-model="criteria"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
placeholder="User suche"
|
||||
></b-input>
|
||||
<user-table
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
type="text"
|
||||
class="test-input-criteria"
|
||||
v-model="criteria"
|
||||
:placeholder="$t('user_search')"
|
||||
></b-form-input>
|
||||
|
||||
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
|
||||
<b-input-group-text class="pointer">
|
||||
<b-icon icon="x" />
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
<select-users-table
|
||||
v-if="itemsList.length > 0"
|
||||
type="UserListSearch"
|
||||
:itemsUser="itemsList"
|
||||
:fieldsTable="Searchfields"
|
||||
:criteria="criteria"
|
||||
:creation="creation"
|
||||
@update-item="updateItem"
|
||||
:items="itemsList"
|
||||
:fields="Searchfields"
|
||||
@push-item="pushItem"
|
||||
/>
|
||||
<b-pagination
|
||||
pills
|
||||
@ -27,16 +32,21 @@
|
||||
></b-pagination>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
|
||||
<user-table
|
||||
v-show="itemsMassCreation.length > 0"
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
type="UserListMassCreation"
|
||||
:itemsUser="itemsMassCreation"
|
||||
:fieldsTable="fields"
|
||||
:criteria="null"
|
||||
:creation="creation"
|
||||
@update-item="updateItem"
|
||||
/>
|
||||
<div v-show="itemsMassCreation.length > 0">
|
||||
<div class="text-right pr-4 mb-1">
|
||||
<b-button @click="removeAllBookmarks()" variant="light">
|
||||
<b-icon icon="x" scale="2" variant="danger"></b-icon>
|
||||
|
||||
{{ $t('remove_all') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<selected-users-table
|
||||
class="shadow p-3 mb-5 bg-white rounded"
|
||||
:items="itemsMassCreation"
|
||||
:fields="fields"
|
||||
@remove-item="removeItem"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="itemsMassCreation.length === 0">
|
||||
{{ $t('multiple_creation_text') }}
|
||||
</div>
|
||||
@ -45,7 +55,8 @@
|
||||
type="massCreation"
|
||||
:creation="creation"
|
||||
:items="itemsMassCreation"
|
||||
@remove-all-bookmark="removeAllBookmark"
|
||||
@remove-all-bookmark="removeAllBookmarks"
|
||||
@toast-failed-creations="toastFailedCreations"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -53,64 +64,30 @@
|
||||
</template>
|
||||
<script>
|
||||
import CreationFormular from '../components/CreationFormular.vue'
|
||||
import UserTable from '../components/UserTable.vue'
|
||||
import SelectUsersTable from '../components/Tables/SelectUsersTable.vue'
|
||||
import SelectedUsersTable from '../components/Tables/SelectedUsersTable.vue'
|
||||
import { searchUsers } from '../graphql/searchUsers'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
|
||||
export default {
|
||||
name: 'Creation',
|
||||
mixins: [creationMonths],
|
||||
components: {
|
||||
CreationFormular,
|
||||
UserTable,
|
||||
SelectUsersTable,
|
||||
SelectedUsersTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArrays: false,
|
||||
Searchfields: [
|
||||
{ key: 'bookmark', label: 'bookmark' },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
// label: this.$t('open_creation') + 'Jan | Feb | März',
|
||||
label:
|
||||
this.$moment().subtract(2, 'month').format('MMM') +
|
||||
' | ' +
|
||||
this.$moment().subtract(1, 'month').format('MMM') +
|
||||
' | ' +
|
||||
this.$moment().format('MMM'),
|
||||
formatter: (value, key, item) => {
|
||||
return String(value[0]) + ` | ` + String(value[1]) + ` | ` + String(value[2])
|
||||
},
|
||||
},
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
],
|
||||
fields: [
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
// label: this.$t('open_creation') + 'Jan | Feb | März',
|
||||
label:
|
||||
this.$moment().subtract(2, 'month').format('MMM') +
|
||||
' | ' +
|
||||
this.$moment().subtract(1, 'month').format('MMM') +
|
||||
' | ' +
|
||||
this.$moment().format('MMM'),
|
||||
formatter: (value, key, item) => {
|
||||
return String(value[0]) + ` | ` + String(value[1]) + ` | ` + String(value[2])
|
||||
},
|
||||
},
|
||||
{ key: 'bookmark', label: this.$t('remove') },
|
||||
],
|
||||
itemsList: [],
|
||||
itemsMassCreation: [],
|
||||
itemsMassCreation: this.$store.state.userSelectedInMassCreation,
|
||||
radioSelectedMass: '',
|
||||
criteria: '',
|
||||
creation: [null, null, null],
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
perPage: 25,
|
||||
now: Date.now(),
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
@ -126,6 +103,7 @@ export default {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.perPage,
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.rows = result.data.searchUsers.userCount
|
||||
@ -135,35 +113,74 @@ export default {
|
||||
showDetails: false,
|
||||
}
|
||||
})
|
||||
if (this.itemsMassCreation.length !== 0) {
|
||||
const selectedIndices = this.itemsMassCreation.map((item) => item.userId)
|
||||
this.itemsList = this.itemsList.filter((item) => !selectedIndices.includes(item.userId))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updateItem(e, event) {
|
||||
let index = 0
|
||||
let findArr = {}
|
||||
|
||||
switch (event) {
|
||||
case 'push':
|
||||
findArr = this.itemsList.find((item) => e.userId === item.userId)
|
||||
index = this.itemsList.indexOf(findArr)
|
||||
this.itemsList.splice(index, 1)
|
||||
this.itemsMassCreation.push(findArr)
|
||||
break
|
||||
case 'remove':
|
||||
findArr = this.itemsMassCreation.find((item) => e.userId === item.userId)
|
||||
index = this.itemsMassCreation.indexOf(findArr)
|
||||
this.itemsMassCreation.splice(index, 1)
|
||||
this.itemsList.push(findArr)
|
||||
break
|
||||
default:
|
||||
throw new Error(event)
|
||||
}
|
||||
pushItem(selectedItem) {
|
||||
this.itemsMassCreation = [
|
||||
this.itemsList.find((item) => selectedItem.userId === item.userId),
|
||||
...this.itemsMassCreation,
|
||||
]
|
||||
this.itemsList = this.itemsList.filter((item) => selectedItem.userId !== item.userId)
|
||||
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
|
||||
},
|
||||
removeAllBookmark() {
|
||||
this.itemsMassCreation.forEach((item) => this.itemsList.push(item))
|
||||
removeItem(selectedItem) {
|
||||
this.itemsList = [
|
||||
this.itemsMassCreation.find((item) => selectedItem.userId === item.userId),
|
||||
...this.itemsList,
|
||||
]
|
||||
this.itemsMassCreation = this.itemsMassCreation.filter(
|
||||
(item) => selectedItem.userId !== item.userId,
|
||||
)
|
||||
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
|
||||
},
|
||||
removeAllBookmarks() {
|
||||
this.itemsMassCreation = []
|
||||
this.$store.commit('setUserSelectedInMassCreation', [])
|
||||
this.getUsers()
|
||||
},
|
||||
toastFailedCreations(failedCreations) {
|
||||
failedCreations.forEach((email) =>
|
||||
this.toastError(this.$t('creation_form.creation_failed', { email })),
|
||||
)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
Searchfields() {
|
||||
return [
|
||||
{ key: 'bookmark', label: 'bookmark' },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
label: this.creationLabel,
|
||||
formatter: (value, key, item) => {
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
]
|
||||
},
|
||||
fields() {
|
||||
return [
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
label: this.creationLabel,
|
||||
formatter: (value, key, item) => {
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
{ key: 'bookmark', label: this.$t('remove') },
|
||||
]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
const toastedErrorMock = jest.fn()
|
||||
const toastedSuccessMock = jest.fn()
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
@ -26,7 +26,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
lastName: 'Hotzenplotz',
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergatert',
|
||||
memo: 'Gut Ergattert',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
@ -38,6 +38,7 @@ const apolloMutateMock = jest.fn().mockResolvedValue({})
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
},
|
||||
@ -45,15 +46,6 @@ const mocks = {
|
||||
query: apolloQueryMock,
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastedErrorMock,
|
||||
success: toastedSuccessMock,
|
||||
},
|
||||
$moment: jest.fn((value) => {
|
||||
return {
|
||||
format: jest.fn((format) => value),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
describe('CreationConfirm', () => {
|
||||
@ -73,46 +65,23 @@ describe('CreationConfirm', () => {
|
||||
expect(wrapper.find('div.creation-confirm').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has two pending creations', () => {
|
||||
expect(wrapper.vm.pendingCreations).toHaveLength(2)
|
||||
})
|
||||
|
||||
describe('store', () => {
|
||||
it('commits resetOpenCreations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
|
||||
})
|
||||
|
||||
it('commits setOpenCreations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete creation delete with success', () => {
|
||||
describe('remove creation with success', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
{
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergatert',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', { id: 1 }, 'remove')
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the deletePendingCreation mutation', () => {
|
||||
@ -127,85 +96,85 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_delete')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete creation delete with error', () => {
|
||||
describe('remove creation with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', { id: 1 }, 'remove')
|
||||
await wrapper.findAll('tr').at(1).findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouchhh!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation delete with success', () => {
|
||||
describe('confirm creation with success', () => {
|
||||
beforeEach(async () => {
|
||||
apolloQueryMock.mockResolvedValue({
|
||||
data: {
|
||||
getPendingCreations: [
|
||||
{
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergatert',
|
||||
date: new Date(),
|
||||
moderator: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirmed')
|
||||
apolloMutateMock.mockResolvedValue({})
|
||||
await wrapper.findAll('tr').at(2).findAll('button').at(2).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the deletePendingCreation mutation', () => {
|
||||
expect(apolloMutateMock).not.toBeCalledWith({
|
||||
mutation: deletePendingCreation,
|
||||
variables: { id: 1 },
|
||||
describe('overlay', () => {
|
||||
it('opens the overlay', () => {
|
||||
expect(wrapper.find('#overlay').isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('cancel confirmation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(0).trigger('click')
|
||||
})
|
||||
|
||||
it('closes the overlay', async () => {
|
||||
expect(wrapper.find('#overlay').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('still has 2 items in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('calls the confirmPendingCreation mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalledWith({
|
||||
mutation: confirmPendingCreation,
|
||||
variables: { id: 2 },
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('creation_form.toasted_created')
|
||||
})
|
||||
|
||||
it('has 1 item left in the table', () => {
|
||||
expect(wrapper.findAll('tbody > tr')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm creation with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'Ouchhh!' })
|
||||
await wrapper.find('#overlay').findAll('button').at(1).trigger('click')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouchhh!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('commits openCreationsMinus to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('openCreationsMinus', 1)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastedSuccessMock).toBeCalledWith('Pending Creation has been deleted')
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete creation delete with error', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'UserTable' })
|
||||
.vm.$emit('remove-confirm-result', { id: 1 }, 'confirm')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Case confirm is not supported')
|
||||
})
|
||||
})
|
||||
|
||||
describe('server response is error', () => {
|
||||
describe('server response for get pending creations is error', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
apolloQueryMock.mockRejectedValue({
|
||||
@ -215,7 +184,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('toast an error message', () => {
|
||||
expect(toastedErrorMock).toBeCalledWith('Ouch!')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,85 +1,71 @@
|
||||
<template>
|
||||
<div class="creation-confirm">
|
||||
<user-table
|
||||
<div v-if="overlay" id="overlay" @dblclick="overlay = false">
|
||||
<overlay :item="item" @overlay-cancel="overlay = false" @confirm-creation="confirmCreation" />
|
||||
</div>
|
||||
<open-creations-table
|
||||
class="mt-4"
|
||||
type="PageCreationConfirm"
|
||||
:itemsUser="confirmResult"
|
||||
:fieldsTable="fields"
|
||||
@remove-confirm-result="removeConfirmResult"
|
||||
:items="pendingCreations"
|
||||
:fields="fields"
|
||||
@remove-creation="removeCreation"
|
||||
@show-overlay="showOverlay"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UserTable from '../components/UserTable.vue'
|
||||
import Overlay from '../components/Overlay.vue'
|
||||
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
||||
import { getPendingCreations } from '../graphql/getPendingCreations'
|
||||
import { deletePendingCreation } from '../graphql/deletePendingCreation'
|
||||
import { confirmPendingCreation } from '../graphql/confirmPendingCreation'
|
||||
|
||||
export default {
|
||||
name: 'CreationConfirm',
|
||||
components: {
|
||||
UserTable,
|
||||
OpenCreationsTable,
|
||||
Overlay,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArrays: false,
|
||||
fields: [
|
||||
{ key: 'bookmark', label: 'löschen' },
|
||||
{ key: 'email', label: 'Email' },
|
||||
{ key: 'firstName', label: 'Vorname' },
|
||||
{ key: 'lastName', label: 'Nachname' },
|
||||
{
|
||||
key: 'amount',
|
||||
label: 'Schöpfung',
|
||||
formatter: (value) => {
|
||||
return value + ' GDD'
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: 'Text' },
|
||||
{
|
||||
key: 'date',
|
||||
label: 'Datum',
|
||||
formatter: (value) => {
|
||||
return this.$moment(value).format('ll')
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: 'Moderator' },
|
||||
{ key: 'edit_creation', label: 'ändern' },
|
||||
{ key: 'confirm', label: 'speichern' },
|
||||
],
|
||||
confirmResult: [],
|
||||
pendingCreations: [],
|
||||
overlay: false,
|
||||
item: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeConfirmResult(e, event) {
|
||||
let index = 0
|
||||
const findArr = this.confirmResult.find((arr) => arr.id === e.id)
|
||||
switch (event) {
|
||||
case 'remove':
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deletePendingCreation,
|
||||
variables: {
|
||||
id: findArr.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
index = this.confirmResult.indexOf(findArr)
|
||||
this.confirmResult.splice(index, 1)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
this.$toasted.success('Pending Creation has been deleted')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
})
|
||||
break
|
||||
case 'confirmed':
|
||||
this.confirmResult.splice(index, 1)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
this.$toasted.success('Pending Creation has been deleted')
|
||||
break
|
||||
default:
|
||||
this.$toasted.error('Case ' + event + ' is not supported')
|
||||
}
|
||||
removeCreation(item) {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: deletePendingCreation,
|
||||
variables: {
|
||||
id: item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.updatePendingCreations(item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_delete'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
confirmCreation() {
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: confirmPendingCreation,
|
||||
variables: {
|
||||
id: this.item.id,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.overlay = false
|
||||
this.updatePendingCreations(this.item.id)
|
||||
this.toastSuccess(this.$t('creation_form.toasted_created'))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.overlay = false
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
getPendingCreations() {
|
||||
this.$apollo
|
||||
@ -89,16 +75,69 @@ export default {
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('resetOpenCreations')
|
||||
this.confirmResult = result.data.getPendingCreations
|
||||
this.pendingCreations = result.data.getPendingCreations
|
||||
this.$store.commit('setOpenCreations', result.data.getPendingCreations.length)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updatePendingCreations(id) {
|
||||
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
},
|
||||
showOverlay(item) {
|
||||
this.overlay = true
|
||||
this.item = item
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return [
|
||||
{ key: 'bookmark', label: this.$t('delete') },
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'amount',
|
||||
label: this.$t('creation'),
|
||||
formatter: (value) => {
|
||||
return value + ' GDD'
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: this.$t('text') },
|
||||
{
|
||||
key: 'date',
|
||||
label: this.$t('date'),
|
||||
formatter: (value) => {
|
||||
return this.$d(new Date(value), 'short')
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: this.$t('moderator') },
|
||||
{ key: 'edit_creation', label: this.$t('edit') },
|
||||
{ key: 'confirm', label: this.$t('save') },
|
||||
]
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.getPendingCreations()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
#overlay {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding-left: 5%;
|
||||
background-color: rgba(12, 11, 11, 0.781);
|
||||
z-index: 1000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import UserSearch from './UserSearch.vue'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -9,37 +10,52 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
userCount: 1,
|
||||
userList: [
|
||||
{
|
||||
userId: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
creation: [200, 400, 600],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 2,
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
email: 'benjamin@bluemchen.de',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
creation: [0, 0, 0],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 4,
|
||||
firstName: 'New',
|
||||
lastName: 'User',
|
||||
email: 'new@user.ch',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const toastErrorMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => String(d)),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$toasted: {
|
||||
error: toastErrorMock,
|
||||
},
|
||||
$moment: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
subtract: jest.fn(() => {
|
||||
return {
|
||||
format: jest.fn((m) => m),
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
describe('UserSearch', () => {
|
||||
@ -51,6 +67,7 @@ describe('UserSearch', () => {
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
@ -58,13 +75,130 @@ describe('UserSearch', () => {
|
||||
expect(wrapper.find('div.user-search').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('calls the API', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
notActivated: false,
|
||||
isDeleted: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('unconfirmed emails', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.btn-block').trigger('click')
|
||||
await wrapper.find('button.unconfirmedRegisterMails').trigger('click')
|
||||
})
|
||||
|
||||
it('filters the users by unconfirmed emails', () => {
|
||||
expect(wrapper.vm.searchResult).toHaveLength(1)
|
||||
it('calls API with filter', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
notActivated: true,
|
||||
isDeleted: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleted Users', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.deletedUserSearch').trigger('click')
|
||||
})
|
||||
|
||||
it('calls API with filter', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
notActivated: false,
|
||||
isDeleted: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pagination', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper.setData({ currentPage: 2 })
|
||||
})
|
||||
|
||||
it('calls the API with new page', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 2,
|
||||
pageSize: 25,
|
||||
notActivated: false,
|
||||
isDeleted: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user search', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper.setData({ criteria: 'search string' })
|
||||
})
|
||||
|
||||
it('calls the API with search string', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: 'search string',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
notActivated: false,
|
||||
isDeleted: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('reset the search field', () => {
|
||||
it('calls the API with empty criteria', async () => {
|
||||
jest.clearAllMocks()
|
||||
await wrapper.find('.test-click-clear-criteria').trigger('click')
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
notActivated: false,
|
||||
isDeleted: false,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete user', () => {
|
||||
const now = new Date()
|
||||
beforeEach(async () => {
|
||||
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', 4, now)
|
||||
})
|
||||
|
||||
it('marks the user as deleted', () => {
|
||||
expect(wrapper.vm.searchResult.find((obj) => obj.userId === 4).deletedAt).toEqual(now)
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('user_deleted')
|
||||
})
|
||||
})
|
||||
|
||||
@ -77,7 +211,7 @@ describe('UserSearch', () => {
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorMock).toBeCalledWith('Ouch')
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,25 +1,36 @@
|
||||
<template>
|
||||
<div class="user-search">
|
||||
<div style="text-align: right">
|
||||
<b-button block variant="danger" @click="unconfirmedRegisterMails">
|
||||
<b-icon icon="envelope" variant="light"></b-icon>
|
||||
<b-button class="unconfirmedRegisterMails" variant="light" @click="unconfirmedRegisterMails">
|
||||
<b-icon icon="envelope" variant="danger"></b-icon>
|
||||
{{ filterCheckedEmails ? $t('all_emails') : $t('unregistered_emails') }}
|
||||
</b-button>
|
||||
<b-button class="deletedUserSearch" variant="light" @click="deletedUserSearch">
|
||||
<b-icon icon="x-circle" variant="danger"></b-icon>
|
||||
{{ filterDeletedUser ? $t('all_emails') : $t('deleted_user') }}
|
||||
</b-button>
|
||||
</div>
|
||||
<label>{{ $t('user_search') }}</label>
|
||||
<b-input
|
||||
type="text"
|
||||
v-model="criteria"
|
||||
class="shadow p-3 mb-3 bg-white rounded"
|
||||
:placeholder="$t('user_search')"
|
||||
@input="getUsers"
|
||||
></b-input>
|
||||
|
||||
<user-table
|
||||
<div>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
type="text"
|
||||
class="test-input-criteria"
|
||||
v-model="criteria"
|
||||
:placeholder="$t('user_search')"
|
||||
></b-form-input>
|
||||
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
|
||||
<b-input-group-text class="pointer">
|
||||
<b-icon icon="x" />
|
||||
</b-input-group-text>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</div>
|
||||
<search-user-table
|
||||
type="PageUserSearch"
|
||||
:itemsUser="searchResult"
|
||||
:fieldsTable="fields"
|
||||
:criteria="criteria"
|
||||
:items="searchResult"
|
||||
:fields="fields"
|
||||
@updateDeletedAt="updateDeletedAt"
|
||||
/>
|
||||
<b-pagination
|
||||
pills
|
||||
@ -33,52 +44,28 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import UserTable from '../components/UserTable.vue'
|
||||
import SearchUserTable from '../components/Tables/SearchUserTable.vue'
|
||||
import { searchUsers } from '../graphql/searchUsers'
|
||||
import { creationMonths } from '../mixins/creationMonths'
|
||||
|
||||
export default {
|
||||
name: 'UserSearch',
|
||||
mixins: [creationMonths],
|
||||
components: {
|
||||
UserTable,
|
||||
SearchUserTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showArrays: false,
|
||||
fields: [
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
label: [
|
||||
this.$moment().subtract(2, 'month').format('MMM'),
|
||||
this.$moment().subtract(1, 'month').format('MMM'),
|
||||
this.$moment().format('MMM'),
|
||||
].join(' | '),
|
||||
formatter: (value, key, item) => {
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
{ key: 'show_details', label: this.$t('details') },
|
||||
{ key: 'confirm_mail', label: this.$t('confirmed') },
|
||||
{ key: 'transactions_list', label: this.$t('transaction') },
|
||||
],
|
||||
searchResult: [],
|
||||
massCreation: [],
|
||||
criteria: '',
|
||||
currentMonth: {
|
||||
short: this.$moment().format('MMMM'),
|
||||
},
|
||||
lastMonth: {
|
||||
short: this.$moment().subtract(1, 'month').format('MMMM'),
|
||||
},
|
||||
beforeLastMonth: {
|
||||
short: this.$moment().subtract(2, 'month').format('MMMM'),
|
||||
},
|
||||
filterCheckedEmails: false,
|
||||
filterDeletedUser: false,
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
perPage: 25,
|
||||
now: Date.now(),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -86,6 +73,10 @@ export default {
|
||||
this.filterCheckedEmails = !this.filterCheckedEmails
|
||||
this.getUsers()
|
||||
},
|
||||
deletedUserSearch() {
|
||||
this.filterDeletedUser = !this.filterDeletedUser
|
||||
this.getUsers()
|
||||
},
|
||||
getUsers() {
|
||||
this.$apollo
|
||||
.query({
|
||||
@ -95,6 +86,7 @@ export default {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.perPage,
|
||||
notActivated: this.filterCheckedEmails,
|
||||
isDeleted: this.filterDeletedUser,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
@ -102,14 +94,42 @@ export default {
|
||||
this.searchResult = result.data.searchUsers.userList
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toasted.error(error.message)
|
||||
this.toastError(error.message)
|
||||
})
|
||||
},
|
||||
updateDeletedAt(userId, deletedAt) {
|
||||
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt
|
||||
this.toastSuccess(this.$t('user_deleted'))
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.getUsers()
|
||||
},
|
||||
criteria() {
|
||||
this.getUsers()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return [
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'creation',
|
||||
label: this.creationLabel,
|
||||
formatter: (value, key, item) => {
|
||||
return value.join(' | ')
|
||||
},
|
||||
},
|
||||
// { key: 'show_details', label: this.$t('details') },
|
||||
// { key: 'confirm_mail', label: this.$t('confirmed') },
|
||||
// { key: 'has_elopage', label: 'elopage' },
|
||||
// { key: 'transactions_list', label: this.$t('transaction') },
|
||||
{ key: 'status', label: this.$t('status') },
|
||||
]
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getUsers()
|
||||
|
||||
@ -24,6 +24,9 @@ export const mutations = {
|
||||
moderator: (state, moderator) => {
|
||||
state.moderator = moderator
|
||||
},
|
||||
setUserSelectedInMassCreation: (state, userSelectedInMassCreation) => {
|
||||
state.userSelectedInMassCreation = userSelectedInMassCreation
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
@ -44,6 +47,7 @@ const store = new Vuex.Store({
|
||||
token: CONFIG.DEBUG_DISABLE_AUTH ? 'validToken' : null,
|
||||
moderator: null,
|
||||
openCreations: 0,
|
||||
userSelectedInMassCreation: [],
|
||||
},
|
||||
// Syncronous mutation of the state
|
||||
mutations,
|
||||
|
||||
@ -10,6 +10,7 @@ const {
|
||||
resetOpenCreations,
|
||||
setOpenCreations,
|
||||
moderator,
|
||||
setUserSelectedInMassCreation,
|
||||
} = mutations
|
||||
const { logout } = actions
|
||||
|
||||
@ -64,6 +65,14 @@ describe('Vuex store', () => {
|
||||
expect(state.openCreations).toEqual(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('setUserSelectedInMassCreation', () => {
|
||||
it('sets userSelectedInMassCreation to given value', () => {
|
||||
const state = { userSelectedInMassCreation: [] }
|
||||
setUserSelectedInMassCreation(state, [0, 1, 2])
|
||||
expect(state.userSelectedInMassCreation).toEqual([0, 1, 2])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions', () => {
|
||||
|
||||
@ -5,11 +5,18 @@ import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
|
||||
// without this async calls are not working
|
||||
import 'regenerator-runtime'
|
||||
|
||||
import { toasters } from '../src/mixins/toaster'
|
||||
|
||||
export const toastErrorSpy = jest.spyOn(toasters.methods, 'toastError')
|
||||
export const toastSuccessSpy = jest.spyOn(toasters.methods, 'toastSuccess')
|
||||
|
||||
global.localVue = createLocalVue()
|
||||
|
||||
global.localVue.use(BootstrapVue)
|
||||
global.localVue.use(IconsPlugin)
|
||||
|
||||
global.localVue.mixin(toasters)
|
||||
|
||||
// throw errors for vue warnings to force the programmers to take care about warnings
|
||||
Vue.config.warnHandler = (w) => {
|
||||
throw new Error(w)
|
||||
|
||||
@ -3634,16 +3634,16 @@ bootstrap-vue@^2.21.2:
|
||||
portal-vue "^2.1.7"
|
||||
vue-functional-data-merge "^3.1.0"
|
||||
|
||||
bootstrap@4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac"
|
||||
integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==
|
||||
|
||||
"bootstrap@>=4.5.3 <5.0.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
|
||||
integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==
|
||||
|
||||
bootstrap@^5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34"
|
||||
integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@ -9032,11 +9032,6 @@ mkdirp@0.x, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
moment@^2.19.2, moment@^2.29.1:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
@ -12491,13 +12486,6 @@ vue-loader@^15.9.2:
|
||||
vue-hot-reload-api "^2.3.0"
|
||||
vue-style-loader "^4.1.0"
|
||||
|
||||
vue-moment@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/vue-moment/-/vue-moment-4.1.0.tgz#092a8ff723a96c6f85a0a8e23ad30f0bf320f3b0"
|
||||
integrity sha512-Gzisqpg82ItlrUyiD9d0Kfru+JorW2o4mQOH06lEDZNgxci0tv/fua1Hl0bo4DozDV2JK1r52Atn/8QVCu8qQw==
|
||||
dependencies:
|
||||
moment "^2.19.2"
|
||||
|
||||
vue-router@^3.5.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.3.tgz#041048053e336829d05dafacf6a8fb669a2e7999"
|
||||
@ -12524,11 +12512,6 @@ vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
|
||||
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
|
||||
|
||||
vue-toasted@^1.1.28:
|
||||
version "1.1.28"
|
||||
resolved "https://registry.yarnpkg.com/vue-toasted/-/vue-toasted-1.1.28.tgz#dbabb83acc89f7a9e8765815e491d79f0dc65c26"
|
||||
integrity sha512-UUzr5LX51UbbiROSGZ49GOgSzFxaMHK6L00JV8fir/CYNJCpIIvNZ5YmS4Qc8Y2+Z/4VVYRpeQL2UO0G800Raw==
|
||||
|
||||
vue@^2.6.11:
|
||||
version "2.6.14"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.14.tgz#e51aa5250250d569a3fbad3a8a5a687d6036e235"
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
@ -10,29 +13,34 @@ DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
#EMAIL=true
|
||||
#EMAIL_USERNAME=
|
||||
#EMAIL_SENDER=
|
||||
#EMAIL_PASSWORD=
|
||||
#EMAIL_SMTP_URL=
|
||||
#EMAIL_SMTP_PORT=587
|
||||
#RESEND_TIME=1 minute, 60 => 1hour, 1440 (60 minutes * 24 hours) => 24 hours
|
||||
#RESEND_TIME=
|
||||
RESEND_TIME=10
|
||||
# Klicktipp
|
||||
KLICKTIPP=false
|
||||
KLICKTTIPP_API_URL=https://api.klicktipp.com
|
||||
KLICKTIPP_USER=gradido_test
|
||||
KLICKTIPP_PASSWORD=secret321
|
||||
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
#EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{code}
|
||||
#EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code}
|
||||
# Community
|
||||
COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
|
||||
#KLICKTIPP_USER=
|
||||
#KLICKTIPP_PASSWORD=
|
||||
#KLICKTIPP_APIKEY_DE=
|
||||
#KLICKTIPP_APIKEY_EN=
|
||||
#KLICKTIPP=true
|
||||
COMMUNITY_NAME=
|
||||
COMMUNITY_URL=
|
||||
COMMUNITY_REGISTER_URL=
|
||||
COMMUNITY_DESCRIPTION=
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
EMAIL_SMTP_URL=gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION=http://localhost/checkEmail/{code}
|
||||
EMAIL_LINK_SETPASSWORD=http://localhost/reset/{code}
|
||||
RESEND_TIME=10
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
@ -1,8 +1,10 @@
|
||||
PORT=4000
|
||||
# Server
|
||||
JWT_SECRET=$JWT_SECRET
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
GDT_API_URL=$GDT_API_URL
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=$DB_USER
|
||||
@ -10,26 +12,35 @@ DB_PASSWORD=$DB_PASSWORD
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
|
||||
|
||||
# Klicktipp
|
||||
KLICKTIPP=$KLICKTIPP
|
||||
KLICKTTIPP_API_URL=https://api.klicktipp.com
|
||||
KLICKTIPP_USER=$KLICKTIPP_USER
|
||||
KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD
|
||||
KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
|
||||
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=$COMMUNITY_NAME
|
||||
COMMUNITY_URL=$COMMUNITY_URL
|
||||
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
RESEND_TIME=10
|
||||
EMAIL=$EMAIL
|
||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||
EMAIL_SENDER=$EMAIL_SENDER
|
||||
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
||||
EMAIL_SMTP_URL=$EMAIL_SMTP_URL
|
||||
EMAIL_SMTP_PORT=587
|
||||
#RESEND_TIME=1 minute, 60 => 1hour, 1440 (60 minutes * 24 hours) => 24 hours
|
||||
RESEND_TIME=10
|
||||
|
||||
EMAIL_LINK_VERIFICATION=$EMAIL_LINK_VERIFICATION
|
||||
EMAIL_LINK_SETPASSWORD=$EMAIL_LINK_SETPASSWORD
|
||||
RESEND_TIME=10
|
||||
|
||||
#KLICKTIPP_USER=
|
||||
#KLICKTIPP_PASSWORD=
|
||||
#KLICKTIPP_APIKEY_DE=
|
||||
#KLICKTIPP_APIKEY_EN=
|
||||
#KLICKTIPP=true
|
||||
COMMUNITY_NAME=
|
||||
COMMUNITY_URL=
|
||||
COMMUNITY_REGISTER_URL=
|
||||
COMMUNITY_DESCRIPTION=
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@ -1,5 +1,6 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/.env.bak
|
||||
/build/
|
||||
package-json.lock
|
||||
coverage
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC'
|
||||
return {
|
||||
verbose: true,
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
|
||||
moduleNameMapper: {
|
||||
'@entity/(.*)': '<rootDir>/../database/build/entity/$1',
|
||||
// This is hack to fix a problem with the library `ts-mysql-migrate` which does differentiate between its ts/js state
|
||||
'@dbTools/(.*)':
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
},
|
||||
}
|
||||
module.exports = {
|
||||
verbose: true,
|
||||
preset: 'ts-jest',
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**'],
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
moduleNameMapper: {
|
||||
'@entity/(.*)':
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/entity/$1'
|
||||
: '<rootDir>/../database/build/entity/$1',
|
||||
'@dbTools/(.*)':
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.5.1",
|
||||
"version": "1.6.6",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -10,35 +10,35 @@
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "node build/index.js",
|
||||
"start": "node build/src/index.js",
|
||||
"dev": "nodemon -w src --ext ts --exec ts-node src/index.ts",
|
||||
"lint": "eslint . --ext .js,.ts",
|
||||
"CI_worklfow_test": "jest --runInBand --coverage ",
|
||||
"test": "NODE_ENV=development jest --runInBand --coverage "
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||
"test": "TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"apollo-log": "^1.1.0",
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"apollo-server-testing": "^2.25.2",
|
||||
"axios": "^0.21.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"cors": "^2.8.5",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.5.1",
|
||||
"jest": "^27.2.4",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"module-alias": "^2.2.2",
|
||||
"moment": "^2.29.1",
|
||||
"mysql2": "^2.3.0",
|
||||
"nodemailer": "^6.6.5",
|
||||
"random-bigint": "^0.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"sodium-native": "^3.3.0",
|
||||
"ts-jest": "^27.0.5",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typeorm": "^0.2.38"
|
||||
"type-graphql": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.12",
|
||||
|
||||
@ -7,5 +7,4 @@ export const INALIENABLE_RIGHTS = [
|
||||
RIGHTS.CREATE_USER,
|
||||
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
|
||||
RIGHTS.SET_PASSWORD,
|
||||
RIGHTS.CHECK_USERNAME,
|
||||
]
|
||||
|
||||
@ -17,7 +17,6 @@ export enum RIGHTS {
|
||||
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
|
||||
SET_PASSWORD = 'SET_PASSWORD',
|
||||
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
|
||||
CHECK_USERNAME = 'CHECK_USERNAME',
|
||||
HAS_ELOPAGE = 'HAS_ELOPAGE',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
@ -26,4 +25,7 @@ export enum RIGHTS {
|
||||
SEARCH_PENDING_CREATION = 'SEARCH_PENDING_CREATION',
|
||||
DELETE_PENDING_CREATION = 'DELETE_PENDING_CREATION',
|
||||
CONFIRM_PENDING_CREATION = 'CONFIRM_PENDING_CREATION',
|
||||
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
}
|
||||
|
||||
9
backend/src/config/index.test.ts
Normal file
9
backend/src/config/index.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import CONFIG from './index'
|
||||
|
||||
describe('config/index', () => {
|
||||
describe('decay start block', () => {
|
||||
it('has the correct date set', () => {
|
||||
expect(CONFIG.DECAY_START_TIME).toEqual(new Date('2021-05-13 17:46:31'))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,8 +1,19 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import Decimal from 'decimal.js-light'
|
||||
dotenv.config()
|
||||
|
||||
Decimal.set({
|
||||
precision: 25,
|
||||
rounding: Decimal.ROUND_HALF_UP,
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0029-clean_transaction_table',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT || 4000,
|
||||
JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
@ -43,6 +54,7 @@ const loginServer = {
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY || 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
}
|
||||
|
||||
// TODO: Hannes if I find you... this looks like blasphemy
|
||||
const resendTime = parseInt(process.env.RESEND_TIME ? process.env.RESEND_TIME : 'null')
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
@ -66,6 +78,7 @@ const webhook = {
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...klicktipp,
|
||||
|
||||
1
backend/src/config/mnemonic.uncompressed_buffer13116.txt
Normal file
1
backend/src/config/mnemonic.uncompressed_buffer13116.txt
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
export default class CheckUsernameArgs {
|
||||
@Field(() => String)
|
||||
username: string
|
||||
}
|
||||
@ -13,4 +13,7 @@ export default class SearchUsersArgs {
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
notActivated?: boolean
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
isDeleted?: boolean
|
||||
}
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ArgsType()
|
||||
export default class TransactionSendArgs {
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => Number)
|
||||
amount: number
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@ -8,12 +8,6 @@ export default class UpdateUserInfosArgs {
|
||||
@Field({ nullable: true })
|
||||
lastName?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
description?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
username?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
language?: string
|
||||
|
||||
|
||||
@ -5,19 +5,31 @@ import { AuthChecker } from 'type-graphql'
|
||||
import { decode, encode } from '../../auth/JWT'
|
||||
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '../../auth/ROLES'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { INALIENABLE_RIGHTS } from '../../auth/INALIENABLE_RIGHTS'
|
||||
import { ServerUser } from '@entity/ServerUser'
|
||||
|
||||
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
||||
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
||||
|
||||
// Do we have a token?
|
||||
if (context.token) {
|
||||
// Decode the token
|
||||
const decoded = decode(context.token)
|
||||
if (!decoded) {
|
||||
// we always throw on an invalid token
|
||||
throw new Error('403.13 - Client certificate revoked')
|
||||
// Are all rights requested public?
|
||||
const isInalienable = (<RIGHTS[]>rights).reduce(
|
||||
(acc, right) => acc && INALIENABLE_RIGHTS.includes(right),
|
||||
true,
|
||||
)
|
||||
if (isInalienable) {
|
||||
// If public dont throw and permit access
|
||||
return true
|
||||
} else {
|
||||
// Throw on a protected route
|
||||
throw new Error('403.13 - Client certificate revoked')
|
||||
}
|
||||
}
|
||||
// Set context pubKey
|
||||
context.pubKey = Buffer.from(decoded.pubKey).toString('hex')
|
||||
@ -26,8 +38,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
const userRepository = await getCustomRepository(UserRepository)
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const serverUserRepository = await getCustomRepository(ServerUserRepository)
|
||||
const countServerUsers = await serverUserRepository.count({ email: user.email })
|
||||
const countServerUsers = await ServerUser.count({ email: user.email })
|
||||
context.role = countServerUsers > 0 ? ROLE_ADMIN : ROLE_USER
|
||||
|
||||
context.setHeaders.push({ key: 'token', value: encode(decoded.pubKey) })
|
||||
|
||||
@ -3,6 +3,9 @@ import { registerEnumType } from 'type-graphql'
|
||||
export enum TransactionTypeId {
|
||||
CREATION = 1,
|
||||
SEND = 2,
|
||||
RECEIVE = 3,
|
||||
// This is a virtual property, never occurring on the database
|
||||
DECAY = 4,
|
||||
}
|
||||
|
||||
registerEnumType(TransactionTypeId, {
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class Balance {
|
||||
constructor(json: any) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decay = Number(json.decay)
|
||||
this.balance = json.balance
|
||||
this.decay = json.decay
|
||||
this.decayDate = json.decay_date
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
@Field(() => Decimal)
|
||||
decay: Decimal
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
@Field(() => Date)
|
||||
decayDate: Date
|
||||
}
|
||||
|
||||
@ -1,33 +1,34 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@ObjectType()
|
||||
export class Decay {
|
||||
constructor(json: any) {
|
||||
if (json) {
|
||||
this.balance = Number(json.balance)
|
||||
this.decayStart = json.decay_start
|
||||
this.decayEnd = json.decay_end
|
||||
this.decayDuration = json.decay_duration
|
||||
this.decayStartBlock = json.decay_start_block
|
||||
}
|
||||
constructor(
|
||||
balance: Decimal,
|
||||
decay: Decimal,
|
||||
start: Date | null,
|
||||
end: Date | null,
|
||||
duration: number | null,
|
||||
) {
|
||||
this.balance = balance
|
||||
this.decay = decay
|
||||
this.start = start
|
||||
this.end = end
|
||||
this.duration = duration
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStart: string
|
||||
@Field(() => Decimal)
|
||||
decay: Decimal
|
||||
|
||||
// timestamp in seconds
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayEnd: string
|
||||
@Field(() => Date, { nullable: true })
|
||||
start: Date | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
decayDuration?: number
|
||||
@Field(() => Date, { nullable: true })
|
||||
end: Date | null
|
||||
|
||||
@Field(() => Int, { nullable: true })
|
||||
decayStartBlock?: string
|
||||
duration: number | null
|
||||
}
|
||||
|
||||
@ -1,55 +1,70 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { Decay } from './Decay'
|
||||
|
||||
// we need a better solution for the decay block:
|
||||
// the first transaction on the first page shows the decay since the last transaction
|
||||
// the format is actually a Decay and not a Transaction.
|
||||
// Therefore we have a lot of nullable fields, which should be always present
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { User } from './User'
|
||||
|
||||
@ObjectType()
|
||||
export class Transaction {
|
||||
constructor() {
|
||||
this.type = ''
|
||||
this.balance = 0
|
||||
this.totalBalance = 0
|
||||
this.memo = ''
|
||||
constructor(transaction: dbTransaction, user: User, linkedUser: User | null = null) {
|
||||
this.id = transaction.id
|
||||
this.user = user
|
||||
this.previous = transaction.previous
|
||||
this.typeId = transaction.typeId
|
||||
this.amount = transaction.amount
|
||||
this.balance = transaction.balance
|
||||
this.balanceDate = transaction.balanceDate
|
||||
if (!transaction.decayStart) {
|
||||
this.decay = new Decay(transaction.balance, new Decimal(0), null, null, null)
|
||||
} else {
|
||||
this.decay = new Decay(
|
||||
transaction.balance,
|
||||
transaction.decay,
|
||||
transaction.decayStart,
|
||||
transaction.balanceDate,
|
||||
Math.round((transaction.balanceDate.getTime() - transaction.decayStart.getTime()) / 1000),
|
||||
)
|
||||
}
|
||||
this.memo = transaction.memo
|
||||
this.creationDate = transaction.creationDate
|
||||
this.linkedUser = linkedUser
|
||||
this.linkedTransactionId = transaction.linkedTransactionId
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
type: string
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
id: number
|
||||
|
||||
@Field(() => Number)
|
||||
totalBalance: number
|
||||
@Field(() => User)
|
||||
user: User
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayStart?: string
|
||||
@Field(() => Number, { nullable: true })
|
||||
previous: number | null
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayEnd?: string
|
||||
@Field(() => TransactionTypeId)
|
||||
typeId: TransactionTypeId
|
||||
|
||||
@Field({ nullable: true })
|
||||
decayDuration?: number
|
||||
@Field(() => Decimal)
|
||||
amount: Decimal
|
||||
|
||||
@Field(() => Decimal)
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Date)
|
||||
balanceDate: Date
|
||||
|
||||
@Field(() => Decay)
|
||||
decay: Decay
|
||||
|
||||
@Field(() => String)
|
||||
memo: string
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
creationDate: Date | null
|
||||
|
||||
@Field(() => User, { nullable: true })
|
||||
linkedUser: User | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
transactionId?: number
|
||||
|
||||
@Field({ nullable: true })
|
||||
name?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
email?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
date?: string
|
||||
|
||||
@Field({ nullable: true })
|
||||
decay?: Decay
|
||||
linkedTransactionId?: number | null
|
||||
}
|
||||
|
||||
@ -1,32 +1,35 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import CONFIG from '../../config'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Transaction } from './Transaction'
|
||||
|
||||
@ObjectType()
|
||||
export class TransactionList {
|
||||
constructor() {
|
||||
this.gdtSum = 0
|
||||
this.count = 0
|
||||
this.balance = 0
|
||||
this.decay = 0
|
||||
this.decayDate = ''
|
||||
constructor(
|
||||
balance: Decimal,
|
||||
transactions: Transaction[],
|
||||
count: number,
|
||||
balanceGDT?: number | null,
|
||||
decayStartBlock: Date = CONFIG.DECAY_START_TIME,
|
||||
) {
|
||||
this.balance = balance
|
||||
this.transactions = transactions
|
||||
this.count = count
|
||||
this.balanceGDT = balanceGDT || null
|
||||
this.decayStartBlock = decayStartBlock
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
gdtSum: number
|
||||
@Field(() => Number, { nullable: true })
|
||||
balanceGDT: number | null
|
||||
|
||||
@Field(() => Number)
|
||||
count: number
|
||||
|
||||
@Field(() => Number)
|
||||
balance: number
|
||||
balance: Decimal
|
||||
|
||||
@Field(() => Number)
|
||||
decay: number
|
||||
|
||||
@Field(() => String)
|
||||
decayDate: string
|
||||
@Field(() => Date)
|
||||
decayStartBlock: Date
|
||||
|
||||
@Field(() => [Transaction])
|
||||
transactions: Transaction[]
|
||||
|
||||
@ -1,90 +1,74 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { KlickTipp } from './KlickTipp'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
/*
|
||||
@Field(() => ID)
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
*/
|
||||
constructor(json?: any) {
|
||||
if (json) {
|
||||
this.id = json.id
|
||||
this.email = json.email
|
||||
this.firstName = json.first_name
|
||||
this.lastName = json.last_name
|
||||
this.username = json.username
|
||||
this.description = json.description
|
||||
this.pubkey = json.public_hex
|
||||
this.language = json.language
|
||||
this.publisherId = json.publisher_id
|
||||
this.isAdmin = json.isAdmin
|
||||
}
|
||||
constructor(user: dbUser) {
|
||||
this.id = user.id
|
||||
this.email = user.email
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.deletedAt = user.deletedAt
|
||||
this.createdAt = user.createdAt
|
||||
this.emailChecked = user.emailChecked
|
||||
this.language = user.language
|
||||
this.publisherId = user.publisherId
|
||||
// TODO
|
||||
this.isAdmin = null
|
||||
this.coinanimation = null
|
||||
this.klickTipp = null
|
||||
this.hasElopage = null
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
// `public_key` binary(32) DEFAULT NULL,
|
||||
// `privkey` binary(80) DEFAULT NULL,
|
||||
|
||||
// TODO privacy issue here
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
@Field(() => String)
|
||||
firstName: string
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
@Field(() => String)
|
||||
lastName: string
|
||||
@Field(() => String, { nullable: true })
|
||||
lastName: string | null
|
||||
|
||||
@Field(() => String)
|
||||
username: string
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
|
||||
@Field(() => String)
|
||||
description: string
|
||||
// `password` bigint(20) unsigned DEFAULT 0,
|
||||
// `email_hash` binary(32) DEFAULT NULL,
|
||||
|
||||
@Field(() => String)
|
||||
pubkey: string
|
||||
/*
|
||||
@Field(() => String)
|
||||
pubkey: string
|
||||
|
||||
// not sure about the type here. Maybe better to have a string
|
||||
@Field(() => number)
|
||||
created: number
|
||||
|
||||
@Field(() =>>> Boolean)
|
||||
emailChecked: boolean
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Boolean)
|
||||
passphraseShown: boolean
|
||||
*/
|
||||
emailChecked: boolean
|
||||
|
||||
@Field(() => String)
|
||||
language: string
|
||||
|
||||
/*
|
||||
@Field(() => Boolean)
|
||||
disabled: boolean
|
||||
*/
|
||||
// This is not the users publisherId, but the one of the users who recommend him
|
||||
@Field(() => Number, { nullable: true })
|
||||
publisherId: number | null
|
||||
|
||||
/* I suggest to have a group as type here
|
||||
@Field(() => ID)
|
||||
groupId: number
|
||||
*/
|
||||
// what is publisherId?
|
||||
@Field(() => Int, { nullable: true })
|
||||
publisherId?: number
|
||||
// `passphrase` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
||||
@Field(() => Boolean)
|
||||
isAdmin: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
coinanimation: boolean
|
||||
|
||||
@Field(() => KlickTipp)
|
||||
klickTipp: KlickTipp
|
||||
// TODO this is a bit inconsistent with what we query from the database
|
||||
// therefore all those fields are now nullable with default value null
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
isAdmin: boolean | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
hasElopage?: boolean
|
||||
coinanimation: boolean | null
|
||||
|
||||
@Field(() => KlickTipp, { nullable: true })
|
||||
klickTipp: KlickTipp | null
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
hasElopage: boolean | null
|
||||
}
|
||||
|
||||
@ -1,7 +1,20 @@
|
||||
import { User } from '@entity/User'
|
||||
import { ObjectType, Field, Int } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class UserAdmin {
|
||||
constructor(user: User, creation: number[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||
this.userId = user.id
|
||||
this.email = user.email
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.creation = creation
|
||||
this.emailChecked = user.emailChecked
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
userId: number
|
||||
|
||||
@ -19,6 +32,15 @@ export class UserAdmin {
|
||||
|
||||
@Field(() => Boolean)
|
||||
emailChecked: boolean
|
||||
|
||||
@Field(() => Boolean)
|
||||
hasElopage: boolean
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt?: Date | null
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
emailConfirmationSend?: string
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@ -2,80 +2,183 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx } from 'type-graphql'
|
||||
import { getCustomRepository, Raw } from 'typeorm'
|
||||
import {
|
||||
getCustomRepository,
|
||||
IsNull,
|
||||
Not,
|
||||
ObjectLiteral,
|
||||
getConnection,
|
||||
In,
|
||||
} from '@dbTools/typeorm'
|
||||
import { UserAdmin, SearchUsersResult } from '../model/UserAdmin'
|
||||
import { PendingCreation } from '../model/PendingCreation'
|
||||
import { CreatePendingCreations } from '../model/CreatePendingCreations'
|
||||
import { UpdatePendingCreation } from '../model/UpdatePendingCreation'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||
import { TransactionCreationRepository } from '../../typeorm/repository/TransactionCreation'
|
||||
import { LoginPendingTasksAdminRepository } from '../../typeorm/repository/LoginPendingTasksAdmin'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import CreatePendingCreationArgs from '../arg/CreatePendingCreationArgs'
|
||||
import UpdatePendingCreationArgs from '../arg/UpdatePendingCreationArgs'
|
||||
import SearchUsersArgs from '../arg/SearchUsersArgs'
|
||||
import moment from 'moment'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { TransactionCreation } from '@entity/TransactionCreation'
|
||||
import { UserTransaction } from '@entity/UserTransaction'
|
||||
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
|
||||
import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
import { AdminPendingCreation } from '@entity/AdminPendingCreation'
|
||||
import { hasElopageBuys } from '../../util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
|
||||
@Resolver()
|
||||
export class AdminResolver {
|
||||
@Authorized([RIGHTS.SEARCH_USERS])
|
||||
@Query(() => SearchUsersResult)
|
||||
async searchUsers(
|
||||
@Args() { searchText, currentPage = 1, pageSize = 25, notActivated = false }: SearchUsersArgs,
|
||||
@Args()
|
||||
{
|
||||
searchText,
|
||||
currentPage = 1,
|
||||
pageSize = 25,
|
||||
notActivated = false,
|
||||
isDeleted = false,
|
||||
}: SearchUsersArgs,
|
||||
): Promise<SearchUsersResult> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const users = await userRepository.findBySearchCriteria(searchText)
|
||||
let adminUsers = await Promise.all(
|
||||
|
||||
const filterCriteria: ObjectLiteral[] = []
|
||||
if (notActivated) {
|
||||
filterCriteria.push({ emailChecked: false })
|
||||
}
|
||||
|
||||
if (isDeleted) {
|
||||
filterCriteria.push({ deletedAt: Not(IsNull()) })
|
||||
}
|
||||
|
||||
const userFields = ['id', 'firstName', 'lastName', 'email', 'emailChecked', 'deletedAt']
|
||||
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
|
||||
userFields.map((fieldName) => {
|
||||
return 'user.' + fieldName
|
||||
}),
|
||||
searchText,
|
||||
filterCriteria,
|
||||
currentPage,
|
||||
pageSize,
|
||||
)
|
||||
|
||||
if (users.length === 0) {
|
||||
return {
|
||||
userCount: 0,
|
||||
userList: [],
|
||||
}
|
||||
}
|
||||
|
||||
const creations = await getUserCreations(users.map((u) => u.id))
|
||||
|
||||
const adminUsers = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
const adminUser = new UserAdmin()
|
||||
adminUser.userId = user.id
|
||||
adminUser.firstName = user.firstName
|
||||
adminUser.lastName = user.lastName
|
||||
adminUser.email = user.email
|
||||
adminUser.creation = await getUserCreations(user.id)
|
||||
adminUser.emailChecked = await hasActivatedEmail(user.email)
|
||||
let emailConfirmationSend = ''
|
||||
if (!user.emailChecked) {
|
||||
const emailOptIn = await LoginEmailOptIn.findOne(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
order: {
|
||||
updatedAt: 'DESC',
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
select: ['updatedAt', 'createdAt'],
|
||||
},
|
||||
)
|
||||
if (emailOptIn) {
|
||||
if (emailOptIn.updatedAt) {
|
||||
emailConfirmationSend = emailOptIn.updatedAt.toISOString()
|
||||
} else {
|
||||
emailConfirmationSend = emailOptIn.createdAt.toISOString()
|
||||
}
|
||||
}
|
||||
}
|
||||
const userCreations = creations.find((c) => c.id === user.id)
|
||||
const adminUser = new UserAdmin(
|
||||
user,
|
||||
userCreations ? userCreations.creations : [1000, 1000, 1000],
|
||||
await hasElopageBuys(user.email),
|
||||
emailConfirmationSend,
|
||||
)
|
||||
return adminUser
|
||||
}),
|
||||
)
|
||||
if (notActivated) adminUsers = adminUsers.filter((u) => !u.emailChecked)
|
||||
const first = (currentPage - 1) * pageSize
|
||||
return {
|
||||
userCount: adminUsers.length,
|
||||
userList: adminUsers.slice(first, first + pageSize),
|
||||
userCount: count,
|
||||
userList: adminUsers,
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_USER])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async deleteUser(@Arg('userId') userId: number, @Ctx() context: any): Promise<Date | null> {
|
||||
const user = await User.findOne({ id: userId })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// moderator user disabled own account?
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const moderatorUser = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
if (moderatorUser.id === userId) {
|
||||
throw new Error('Moderator can not delete his own account!')
|
||||
}
|
||||
// soft-delete user
|
||||
await user.softRemove()
|
||||
const newUser = await User.findOne({ id: userId }, { withDeleted: true })
|
||||
return newUser ? newUser.deletedAt : null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.UNDELETE_USER])
|
||||
@Mutation(() => Date, { nullable: true })
|
||||
async unDeleteUser(@Arg('userId') userId: number): Promise<Date | null> {
|
||||
const user = await User.findOne({ id: userId }, { withDeleted: true })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with userId: ${userId}`)
|
||||
}
|
||||
// recover user account
|
||||
await user.recover()
|
||||
return null
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||
@Mutation(() => [Number])
|
||||
async createPendingCreation(
|
||||
@Args() { email, amount, memo, creationDate, moderator }: CreatePendingCreationArgs,
|
||||
): Promise<number[]> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const user = await userRepository.findByEmail(email)
|
||||
|
||||
const creations = await getUserCreations(user.id)
|
||||
const user = await User.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
throw new Error('This user was deleted. Cannot make a creation.')
|
||||
}
|
||||
if (!user.emailChecked) {
|
||||
throw new Error('Creation could not be saved, Email is not activated')
|
||||
}
|
||||
const creations = await getUserCreation(user.id)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
if (isCreationValid(creations, amount, creationDateObj)) {
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const loginPendingTaskAdmin = loginPendingTasksAdminRepository.create()
|
||||
loginPendingTaskAdmin.userId = user.id
|
||||
loginPendingTaskAdmin.amount = BigInt(amount * 10000)
|
||||
loginPendingTaskAdmin.created = new Date()
|
||||
loginPendingTaskAdmin.date = creationDateObj
|
||||
loginPendingTaskAdmin.memo = memo
|
||||
loginPendingTaskAdmin.moderator = moderator
|
||||
const adminPendingCreation = AdminPendingCreation.create()
|
||||
adminPendingCreation.userId = user.id
|
||||
adminPendingCreation.amount = BigInt(amount * 10000)
|
||||
adminPendingCreation.created = new Date()
|
||||
adminPendingCreation.date = creationDateObj
|
||||
adminPendingCreation.memo = memo
|
||||
adminPendingCreation.moderator = moderator
|
||||
|
||||
await loginPendingTasksAdminRepository.save(loginPendingTaskAdmin)
|
||||
await AdminPendingCreation.save(adminPendingCreation)
|
||||
}
|
||||
return getUserCreations(user.id)
|
||||
return getUserCreation(user.id)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATE_PENDING_CREATION])
|
||||
@ -109,27 +212,42 @@ export class AdminResolver {
|
||||
async updatePendingCreation(
|
||||
@Args() { id, email, amount, memo, creationDate, moderator }: UpdatePendingCreationArgs,
|
||||
): Promise<UpdatePendingCreation> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const user = await userRepository.findByEmail(email)
|
||||
const user = await User.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
throw new Error(`User was deleted (${email})`)
|
||||
}
|
||||
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const updatedCreation = await loginPendingTasksAdminRepository.findOneOrFail({ id })
|
||||
const pendingCreationToUpdate = await AdminPendingCreation.findOneOrFail({ id })
|
||||
|
||||
if (updatedCreation.userId !== user.id)
|
||||
if (pendingCreationToUpdate.userId !== user.id) {
|
||||
throw new Error('user of the pending creation and send user does not correspond')
|
||||
}
|
||||
|
||||
updatedCreation.amount = BigInt(amount * 10000)
|
||||
updatedCreation.memo = memo
|
||||
updatedCreation.date = new Date(creationDate)
|
||||
updatedCreation.moderator = moderator
|
||||
const creationDateObj = new Date(creationDate)
|
||||
let creations = await getUserCreation(user.id)
|
||||
if (pendingCreationToUpdate.date.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, pendingCreationToUpdate)
|
||||
}
|
||||
|
||||
await loginPendingTasksAdminRepository.save(updatedCreation)
|
||||
if (!isCreationValid(creations, amount, creationDateObj)) {
|
||||
throw new Error('Creation is not valid')
|
||||
}
|
||||
pendingCreationToUpdate.amount = BigInt(amount * 10000)
|
||||
pendingCreationToUpdate.memo = memo
|
||||
pendingCreationToUpdate.date = new Date(creationDate)
|
||||
pendingCreationToUpdate.moderator = moderator
|
||||
|
||||
await AdminPendingCreation.save(pendingCreationToUpdate)
|
||||
const result = new UpdatePendingCreation()
|
||||
result.amount = parseInt(amount.toString())
|
||||
result.memo = updatedCreation.memo
|
||||
result.date = updatedCreation.date
|
||||
result.moderator = updatedCreation.moderator
|
||||
result.creation = await getUserCreations(user.id)
|
||||
result.memo = pendingCreationToUpdate.memo
|
||||
result.date = pendingCreationToUpdate.date
|
||||
result.moderator = pendingCreationToUpdate.moderator
|
||||
|
||||
result.creation = await getUserCreation(user.id)
|
||||
|
||||
return result
|
||||
}
|
||||
@ -137,220 +255,176 @@ export class AdminResolver {
|
||||
@Authorized([RIGHTS.SEARCH_PENDING_CREATION])
|
||||
@Query(() => [PendingCreation])
|
||||
async getPendingCreations(): Promise<PendingCreation[]> {
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const pendingCreations = await loginPendingTasksAdminRepository.find()
|
||||
const pendingCreations = await AdminPendingCreation.find()
|
||||
if (pendingCreations.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const pendingCreationsPromise = await Promise.all(
|
||||
pendingCreations.map(async (pendingCreation) => {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const user = await userRepository.findOneOrFail({ id: pendingCreation.userId })
|
||||
const userIds = pendingCreations.map((p) => p.userId)
|
||||
const userCreations = await getUserCreations(userIds)
|
||||
const users = await User.find({ where: { id: In(userIds) }, withDeleted: true })
|
||||
|
||||
const parsedAmount = Number(parseInt(pendingCreation.amount.toString()) / 10000)
|
||||
// pendingCreation.amount = parsedAmount
|
||||
const newPendingCreation = {
|
||||
...pendingCreation,
|
||||
amount: parsedAmount,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.email,
|
||||
creation: await getUserCreations(user.id),
|
||||
}
|
||||
return pendingCreations.map((pendingCreation) => {
|
||||
const user = users.find((u) => u.id === pendingCreation.userId)
|
||||
const creation = userCreations.find((c) => c.id === pendingCreation.userId)
|
||||
|
||||
return newPendingCreation
|
||||
}),
|
||||
)
|
||||
return pendingCreationsPromise.reverse()
|
||||
return {
|
||||
...pendingCreation,
|
||||
amount: Number(parseInt(pendingCreation.amount.toString()) / 10000),
|
||||
firstName: user ? user.firstName : '',
|
||||
lastName: user ? user.lastName : '',
|
||||
email: user ? user.email : '',
|
||||
creation: creation ? creation.creations : [1000, 1000, 1000],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.DELETE_PENDING_CREATION])
|
||||
@Mutation(() => Boolean)
|
||||
async deletePendingCreation(@Arg('id') id: number): Promise<boolean> {
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const entity = await loginPendingTasksAdminRepository.findOneOrFail(id)
|
||||
const res = await loginPendingTasksAdminRepository.delete(entity)
|
||||
const entity = await AdminPendingCreation.findOneOrFail(id)
|
||||
const res = await AdminPendingCreation.delete(entity)
|
||||
return !!res
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CONFIRM_PENDING_CREATION])
|
||||
@Mutation(() => Boolean)
|
||||
async confirmPendingCreation(@Arg('id') id: number, @Ctx() context: any): Promise<boolean> {
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const pendingCreation = await loginPendingTasksAdminRepository.findOneOrFail(id)
|
||||
|
||||
const pendingCreation = await AdminPendingCreation.findOneOrFail(id)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const moderatorUser = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
if (moderatorUser.id === pendingCreation.userId)
|
||||
throw new Error('Moderator can not confirm own pending creation')
|
||||
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const receivedCallDate = new Date()
|
||||
let transaction = new Transaction()
|
||||
transaction.transactionTypeId = 1
|
||||
transaction.memo = pendingCreation.memo
|
||||
transaction.received = receivedCallDate
|
||||
transaction.blockchainTypeId = 1
|
||||
transaction = await transactionRepository.save(transaction)
|
||||
if (!transaction) throw new Error('Could not create transaction')
|
||||
const user = await User.findOneOrFail({ id: pendingCreation.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a creation.')
|
||||
|
||||
const transactionCreationRepository = getCustomRepository(TransactionCreationRepository)
|
||||
let transactionCreation = new TransactionCreation()
|
||||
transactionCreation.transactionId = transaction.id
|
||||
transactionCreation.userId = pendingCreation.userId
|
||||
transactionCreation.amount = parseInt(pendingCreation.amount.toString())
|
||||
transactionCreation.targetDate = pendingCreation.date
|
||||
transactionCreation = await transactionCreationRepository.save(transactionCreation)
|
||||
if (!transactionCreation) throw new Error('Could not create transactionCreation')
|
||||
|
||||
const userTransactionRepository = getCustomRepository(UserTransactionRepository)
|
||||
const lastUserTransaction = await userTransactionRepository.findLastForUser(
|
||||
pendingCreation.userId,
|
||||
)
|
||||
let newBalance = 0
|
||||
if (!lastUserTransaction) {
|
||||
newBalance = 0
|
||||
} else {
|
||||
newBalance = await calculateDecay(
|
||||
lastUserTransaction.balance,
|
||||
lastUserTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
)
|
||||
const creations = await getUserCreation(pendingCreation.userId, false)
|
||||
if (!isCreationValid(creations, Number(pendingCreation.amount) / 10000, pendingCreation.date)) {
|
||||
throw new Error('Creation is not valid!!')
|
||||
}
|
||||
newBalance = Number(newBalance) + Number(parseInt(pendingCreation.amount.toString()))
|
||||
|
||||
const newUserTransaction = new UserTransaction()
|
||||
newUserTransaction.userId = pendingCreation.userId
|
||||
newUserTransaction.transactionId = transaction.id
|
||||
newUserTransaction.transactionTypeId = transaction.transactionTypeId
|
||||
newUserTransaction.balance = Number(newBalance)
|
||||
newUserTransaction.balanceDate = transaction.received
|
||||
const receivedCallDate = new Date()
|
||||
|
||||
await userTransactionRepository.save(newUserTransaction).catch((error) => {
|
||||
throw new Error('Error saving user transaction: ' + error)
|
||||
})
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const lastTransaction = await transactionRepository.findLastForUser(pendingCreation.userId)
|
||||
|
||||
const balanceRepository = getCustomRepository(BalanceRepository)
|
||||
let userBalance = await balanceRepository.findByUser(pendingCreation.userId)
|
||||
let newBalance = new Decimal(0)
|
||||
if (lastTransaction) {
|
||||
newBalance = calculateDecay(
|
||||
lastTransaction.balance,
|
||||
lastTransaction.balanceDate,
|
||||
receivedCallDate,
|
||||
).balance
|
||||
}
|
||||
// TODO pending creations decimal
|
||||
newBalance = newBalance.add(new Decimal(Number(pendingCreation.amount)))
|
||||
|
||||
if (!userBalance) userBalance = balanceRepository.create()
|
||||
userBalance.userId = pendingCreation.userId
|
||||
userBalance.amount = Number(newBalance)
|
||||
userBalance.modified = receivedCallDate
|
||||
userBalance.recordDate = receivedCallDate
|
||||
await balanceRepository.save(userBalance)
|
||||
await loginPendingTasksAdminRepository.delete(pendingCreation)
|
||||
const transaction = new Transaction()
|
||||
transaction.typeId = TransactionTypeId.CREATION
|
||||
transaction.memo = pendingCreation.memo
|
||||
transaction.userId = pendingCreation.userId
|
||||
// TODO pending creations decimal
|
||||
transaction.amount = new Decimal(Number(pendingCreation.amount))
|
||||
transaction.creationDate = pendingCreation.date
|
||||
transaction.balance = newBalance
|
||||
transaction.balanceDate = receivedCallDate
|
||||
await transaction.save()
|
||||
|
||||
await AdminPendingCreation.delete(pendingCreation)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserCreations(id: number): Promise<number[]> {
|
||||
const dateNextMonth = moment().add(1, 'month').format('YYYY-MM') + '-01'
|
||||
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM') + '-01'
|
||||
const beforeLastMonthNumber = moment().subtract(2, 'month').format('M')
|
||||
const lastMonthNumber = moment().subtract(1, 'month').format('M')
|
||||
const currentMonthNumber = moment().format('M')
|
||||
interface CreationMap {
|
||||
id: number
|
||||
creations: number[]
|
||||
}
|
||||
|
||||
const transactionCreationRepository = getCustomRepository(TransactionCreationRepository)
|
||||
const createdAmountsQuery = await transactionCreationRepository
|
||||
.createQueryBuilder('transaction_creations')
|
||||
.select('MONTH(transaction_creations.target_date)', 'target_month')
|
||||
.addSelect('SUM(transaction_creations.amount)', 'sum')
|
||||
.where('transaction_creations.state_user_id = :id', { id })
|
||||
.andWhere({
|
||||
targetDate: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
|
||||
date: dateBeforeLastMonth,
|
||||
endDate: dateNextMonth,
|
||||
async function getUserCreation(id: number, includePending = true): Promise<number[]> {
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
return creations[0] ? creations[0].creations : [1000, 1000, 1000]
|
||||
}
|
||||
|
||||
async function getUserCreations(ids: number[], includePending = true): Promise<CreationMap[]> {
|
||||
const months = getCreationMonths()
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT date AS date, amount AS amount, userId AS userId FROM admin_pending_creations
|
||||
WHERE userId IN (${ids.toString()})
|
||||
AND date >= ${dateFilter}`
|
||||
: ''
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND type_id = ${TransactionTypeId.CREATION}
|
||||
AND creation_date >= ${dateFilter}
|
||||
${unionString}) AS result
|
||||
GROUP BY month, userId
|
||||
ORDER BY date DESC
|
||||
`)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return ids.map((id) => {
|
||||
return {
|
||||
id,
|
||||
creations: months.map((month) => {
|
||||
const creation = unionQuery.find(
|
||||
(raw: { month: string; id: string; creation: number[] }) =>
|
||||
parseInt(raw.month) === month && parseInt(raw.id) === id,
|
||||
)
|
||||
return 1000 - (creation ? Number(creation.sum) / 10000 : 0)
|
||||
}),
|
||||
})
|
||||
.groupBy('target_month')
|
||||
.orderBy('target_month', 'ASC')
|
||||
.getRawMany()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loginPendingTasksAdminRepository = getCustomRepository(LoginPendingTasksAdminRepository)
|
||||
const pendingAmountsQuery = await loginPendingTasksAdminRepository
|
||||
.createQueryBuilder('login_pending_tasks_admin')
|
||||
.select('MONTH(login_pending_tasks_admin.date)', 'target_month')
|
||||
.addSelect('SUM(login_pending_tasks_admin.amount)', 'sum')
|
||||
.where('login_pending_tasks_admin.userId = :id', { id })
|
||||
.andWhere({
|
||||
date: Raw((alias) => `${alias} >= :date and ${alias} < :endDate`, {
|
||||
date: dateBeforeLastMonth,
|
||||
endDate: dateNextMonth,
|
||||
}),
|
||||
})
|
||||
.groupBy('target_month')
|
||||
.orderBy('target_month', 'ASC')
|
||||
.getRawMany()
|
||||
function updateCreations(creations: number[], pendingCreation: AdminPendingCreation): number[] {
|
||||
const index = getCreationIndex(pendingCreation.date.getMonth())
|
||||
|
||||
const map = new Map()
|
||||
if (Array.isArray(createdAmountsQuery) && createdAmountsQuery.length > 0) {
|
||||
createdAmountsQuery.forEach((createdAmount) => {
|
||||
if (!map.has(createdAmount.target_month)) {
|
||||
map.set(createdAmount.target_month, createdAmount.sum)
|
||||
} else {
|
||||
const store = map.get(createdAmount.target_month)
|
||||
map.set(createdAmount.target_month, Number(store) + Number(createdAmount.sum))
|
||||
}
|
||||
})
|
||||
if (index < 0) {
|
||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
||||
}
|
||||
|
||||
if (Array.isArray(pendingAmountsQuery) && pendingAmountsQuery.length > 0) {
|
||||
pendingAmountsQuery.forEach((pendingAmount) => {
|
||||
if (!map.has(pendingAmount.target_month)) {
|
||||
map.set(pendingAmount.target_month, pendingAmount.sum)
|
||||
} else {
|
||||
const store = map.get(pendingAmount.target_month)
|
||||
map.set(pendingAmount.target_month, Number(store) + Number(pendingAmount.sum))
|
||||
}
|
||||
})
|
||||
}
|
||||
const usedCreationBeforeLastMonth = map.get(Number(beforeLastMonthNumber))
|
||||
? Number(map.get(Number(beforeLastMonthNumber))) / 10000
|
||||
: 0
|
||||
const usedCreationLastMonth = map.get(Number(lastMonthNumber))
|
||||
? Number(map.get(Number(lastMonthNumber))) / 10000
|
||||
: 0
|
||||
|
||||
const usedCreationCurrentMonth = map.get(Number(currentMonthNumber))
|
||||
? Number(map.get(Number(currentMonthNumber))) / 10000
|
||||
: 0
|
||||
|
||||
return [
|
||||
1000 - usedCreationBeforeLastMonth,
|
||||
1000 - usedCreationLastMonth,
|
||||
1000 - usedCreationCurrentMonth,
|
||||
]
|
||||
creations[index] += parseInt(pendingCreation.amount.toString())
|
||||
return creations
|
||||
}
|
||||
|
||||
function isCreationValid(creations: number[], amount: number, creationDate: Date) {
|
||||
const dateMonth = moment().format('YYYY-MM')
|
||||
const dateLastMonth = moment().subtract(1, 'month').format('YYYY-MM')
|
||||
const dateBeforeLastMonth = moment().subtract(2, 'month').format('YYYY-MM')
|
||||
const creationDateMonth = moment(creationDate).format('YYYY-MM')
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
let openCreation
|
||||
switch (creationDateMonth) {
|
||||
case dateMonth:
|
||||
openCreation = creations[2]
|
||||
break
|
||||
case dateLastMonth:
|
||||
openCreation = creations[1]
|
||||
break
|
||||
case dateBeforeLastMonth:
|
||||
openCreation = creations[0]
|
||||
break
|
||||
default:
|
||||
throw new Error('CreationDate is not in last three months')
|
||||
if (index < 0) {
|
||||
throw new Error(`No Creation found!`)
|
||||
}
|
||||
|
||||
if (openCreation < amount) {
|
||||
throw new Error(`Open creation (${openCreation}) is less than amount (${amount})`)
|
||||
if (amount > creations[index]) {
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the available amount (${creations[index]} GDD) for this month.`,
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function hasActivatedEmail(email: string): Promise<boolean> {
|
||||
const repository = getCustomRepository(LoginUserRepository)
|
||||
const user = await repository.findByEmail(email)
|
||||
return user ? user.emailChecked : false
|
||||
const getCreationMonths = (): number[] => {
|
||||
const now = new Date(Date.now())
|
||||
return [
|
||||
now.getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
|
||||
].reverse()
|
||||
}
|
||||
|
||||
const getCreationIndex = (month: number): number => {
|
||||
return getCreationMonths().findIndex((el) => el === month + 1)
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { Balance } from '../model/Balance'
|
||||
import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
import { roundFloorFrom4 } from '../../util/round'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
@Resolver()
|
||||
export class BalanceResolver {
|
||||
@ -16,27 +16,28 @@ export class BalanceResolver {
|
||||
@Query(() => Balance)
|
||||
async balance(@Ctx() context: any): Promise<Balance> {
|
||||
// load user and balance
|
||||
const balanceRepository = getCustomRepository(BalanceRepository)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const balanceEntity = await balanceRepository.findByUser(userEntity.id)
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const now = new Date()
|
||||
|
||||
const lastTransaction = await Transaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
)
|
||||
|
||||
// No balance found
|
||||
if (!balanceEntity) {
|
||||
if (!lastTransaction) {
|
||||
return new Balance({
|
||||
balance: 0,
|
||||
decay: 0,
|
||||
balance: new Decimal(0),
|
||||
decay: new Decimal(0),
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
return new Balance({
|
||||
balance: roundFloorFrom4(balanceEntity.amount),
|
||||
decay: roundFloorFrom4(
|
||||
await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now),
|
||||
),
|
||||
balance: lastTransaction.balance,
|
||||
decay: calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
||||
decay_date: now.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
|
||||
import { getCustomRepository } from 'typeorm'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import CONFIG from '../../config'
|
||||
import { GdtEntryList } from '../model/GdtEntryList'
|
||||
import Paginated from '../arg/Paginated'
|
||||
@ -25,13 +25,17 @@ export class GdtResolver {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
|
||||
const resultGDT = await apiGet(
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
||||
)
|
||||
if (!resultGDT.success) {
|
||||
throw new Error(resultGDT.data)
|
||||
try {
|
||||
const resultGDT = await apiGet(
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
||||
)
|
||||
if (!resultGDT.success) {
|
||||
throw new Error(resultGDT.data)
|
||||
}
|
||||
return new GdtEntryList(resultGDT.data)
|
||||
} catch (err: any) {
|
||||
throw new Error('GDT Server is not reachable.')
|
||||
}
|
||||
return new GdtEntryList(resultGDT.data)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.EXIST_PID])
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
/* eslint-disable new-cap */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, getConnection, QueryRunner } from 'typeorm'
|
||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||
|
||||
import CONFIG from '../../config'
|
||||
import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail'
|
||||
@ -16,455 +17,21 @@ import Paginated from '../arg/Paginated'
|
||||
|
||||
import { Order } from '../enum/Order'
|
||||
|
||||
import { BalanceRepository } from '../../typeorm/repository/Balance'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { UserTransactionRepository } from '../../typeorm/repository/UserTransaction'
|
||||
import { TransactionRepository } from '../../typeorm/repository/Transaction'
|
||||
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { UserTransaction as dbUserTransaction } from '@entity/UserTransaction'
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import { TransactionSendCoin as dbTransactionSendCoin } from '@entity/TransactionSendCoin'
|
||||
import { Balance as dbBalance } from '@entity/Balance'
|
||||
|
||||
import { apiPost } from '../../apis/HttpRequest'
|
||||
import { roundFloorFrom4, roundCeilFrom4 } from '../../util/round'
|
||||
import { calculateDecay, calculateDecayWithInterval } from '../../util/decay'
|
||||
import { TransactionTypeId } from '../enum/TransactionTypeId'
|
||||
import { TransactionType } from '../enum/TransactionType'
|
||||
import { hasUserAmount, isHexPublicKey } from '../../util/validate'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
import { calculateBalance, isHexPublicKey } from '../../util/validate'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
|
||||
/*
|
||||
# Test
|
||||
|
||||
## Prepare
|
||||
> sudo systemctl start docker
|
||||
> docker-compose up mariadb
|
||||
> DROP all databases
|
||||
> docker-compose down
|
||||
> docker compose up mariadb database
|
||||
> verify there is exactly one database `gradido_community`
|
||||
|
||||
TODO:
|
||||
INSERT INTO `login_groups` (`id`, `alias`, `name`, `url`, `host`, `home`, `description`) VALUES
|
||||
(1, 'docker', 'docker gradido group', 'localhost', 'nginx', '/', 'gradido test group for docker and stage2 with blockchain db');
|
||||
|
||||
>> Database is cool
|
||||
|
||||
### Start login server
|
||||
> docker-compose up login-server community-server nginx
|
||||
>> Login & community servers and nginx proxy are up and running
|
||||
|
||||
## Build database
|
||||
> cd database
|
||||
> yarn
|
||||
> yarn build
|
||||
> cd ..
|
||||
>> Database has been built successful
|
||||
|
||||
### Start backend (no docker for debugging)
|
||||
> cd backend
|
||||
> yarn
|
||||
> yarn dev
|
||||
>> Backend is up and running
|
||||
|
||||
### Create users
|
||||
> chromium http://localhost:4000/graphql
|
||||
> mutation{createUser(email: "receiver@user.net", firstName: "Receiver", lastName: "user", password: "123!AAAb", language: "de")}
|
||||
> mutation{createUser(email: "sender@user.net", firstName: "Sender", lastName: "user", password: "123!AAAb", language: "de")}
|
||||
> mutation{createUser(email: "creator@user.net", firstName: "Creator", lastName: "user", password: "123!AAAb", language: "de")}
|
||||
>> Verify you have 3 entries in `login_users`, `login_user_backups` and `state_users`
|
||||
|
||||
### make creator an admin
|
||||
> INSERT INTO login_user_roles (id, user_id, role_id) VALUES (NULL, '3', '1');
|
||||
> UPDATE login_users SET email_checked = 1 WHERE id = 3;
|
||||
> uncomment line: 19 in community_server/src/Controller/ServerUsersController.php
|
||||
> chromium http://localhost/server-users/add
|
||||
> create user `creator` `123` `creator@different.net`
|
||||
>> verify you have 1 entry in `server_users`
|
||||
> login with user on http://localhost/server-users
|
||||
> activate server user by changing the corresponding flag in the interface
|
||||
> navigate to http://localhost/transaction-creations/create-multi
|
||||
> create 1000GDD for user sender@user.net
|
||||
> navigate to http://localhost
|
||||
> login with `creator@user.net` `123!AAAb`
|
||||
> confirm transaction (top right corner - click the thingy, click the green button `Transaktion abschließen`)
|
||||
|
||||
### the test:
|
||||
> chromium http://localhost:4000/graphql
|
||||
> query{login(email: "sender@user.net", password: "123!AAAb"){pubkey}}
|
||||
>> copy token from network tab (inspect)
|
||||
> mutation{sendCoins(email: "receiver@user.net", amount: 10.0, memo: "Hier!")}
|
||||
> mutation{sendCoins(email: "receiver@user.net", amount: 10.0, memo: "Hier!")}
|
||||
> Headers: {"Authorization": "Bearer ${token}"}
|
||||
>> Verify via Database that stuff is as it should see `state_balance` & `transaction_send_coins`
|
||||
|
||||
### create decay block
|
||||
> chromium http://localhost/transactions/add
|
||||
> login with `creator` `123`
|
||||
> select `decay start`
|
||||
> press submit
|
||||
> wait for at least 0.02 display of decay on user sender@user.net on old frontend, this should be aprox 10min
|
||||
> chromium http://localhost:4000/graphql
|
||||
> query{login(email: "sender@user.net", password: "123!AAAb"){pubkey}}
|
||||
>> copy token from network tab (inspect)
|
||||
> mutation{sendCoins(email: "receiver@user.net", amount: 10.0, memo: "Hier!")}
|
||||
>> verify in `transaction_send_coins` that a decay was taken into account
|
||||
>> same in `state_balances`
|
||||
>> now check the old frontend
|
||||
>>> sender@user.net should have a decay of 0.02
|
||||
>>> while receiver@user.net should have zero decay on anything (old frontend)
|
||||
|
||||
### Export data
|
||||
> docker-compose up phpmyadmin
|
||||
> chromium http://localhost:8074/
|
||||
> select gradido_community
|
||||
> export
|
||||
> select custom
|
||||
> untick structure
|
||||
> ok
|
||||
|
||||
## Results
|
||||
NOTE: We decided not to write the `transaction_signatures` since its unused. This is the main difference.
|
||||
NOTE: We fixed a bug in the `state_user_transactions code` with the new implementation of apollo
|
||||
|
||||
|
||||
Master:
|
||||
|
||||
--
|
||||
-- Dumping data for table `state_user_transactions`
|
||||
--
|
||||
|
||||
INSERT INTO `state_user_transactions` (`id`, `state_user_id`, `transaction_id`, `transaction_type_id`, `balance`, `balance_date`) VALUES
|
||||
(1, 2, 1, 1, 10000000, '2021-11-05 12:45:18'),
|
||||
(2, 2, 2, 2, 9900000, '2021-11-05 12:48:35'),
|
||||
(3, 1, 2, 2, 100000, '2021-11-05 12:48:35'),
|
||||
(4, 2, 3, 2, 9800000, '2021-11-05 12:49:07'),
|
||||
(5, 1, 3, 2, 200000, '2021-11-05 12:49:07'),
|
||||
(6, 2, 5, 2, 9699845, '2021-11-05 13:03:50'),
|
||||
(7, 1, 5, 2, 99996, '2021-11-05 13:03:50');
|
||||
|
||||
--
|
||||
-- Dumping data for table `transactions`
|
||||
--
|
||||
|
||||
INSERT INTO `transactions` (`id`, `state_group_id`, `transaction_type_id`, `tx_hash`, `memo`, `received`, `blockchain_type_id`) VALUES
|
||||
(1, NULL, 1, 0x9ccdcd01ccb6320c09c2d1da2f0bf735a95ece0e7c1df6bbff51918fbaec061700000000000000000000000000000000, '', '2021-11-05 12:45:18', 1),
|
||||
(2, NULL, 2, 0x58d7706a67fa4ff4b8038168c6be39a2963d7e28e9d3872759ad09c519fe093700000000000000000000000000000000, 'Hier!', '2021-11-05 12:48:35', 1),
|
||||
(3, NULL, 2, 0x427cd214f92ef35af671129d50edc5a478c53d1e464f285b7615d9794a69f69b00000000000000000000000000000000, 'Hier!', '2021-11-05 12:49:07', 1),
|
||||
(4, NULL, 9, 0x32807368f0906a21b94c072599795bc9eeab88fb565df82e85cc62a4fdcde48500000000000000000000000000000000, '', '2021-11-05 12:51:51', 1),
|
||||
(5, NULL, 2, 0x75eb729e0f60a1c8cead1342955853d2440d7a2ea57dfef6d4a18bff0d94491e00000000000000000000000000000000, 'Hier!', '2021-11-05 13:03:50', 1);
|
||||
|
||||
--
|
||||
-- Dumping data for table `transaction_signatures`
|
||||
--
|
||||
|
||||
INSERT INTO `transaction_signatures` (`id`, `transaction_id`, `signature`, `pubkey`) VALUES
|
||||
(1, 1, 0x5888edcdcf77aaadad6d321882903bc831d7416f17213fd5020a764365b5fcb336e4c7917385a1278ea44ccdb31eac4a09e448053b5e3f8f1fe5da3baf53c008, 0xd5b20f8dee415038bfa2b6b0e1b40ff54850351109444863b04d6d28825b7b7d),
|
||||
(2, 2, 0xf6fef428f8f22faf7090f7d740e6088d1d90c58ae92d757117d7d91d799e659f3a3a0c65a3fd97cbde798e761f9d23eff13e8810779a184c97c411f28e7c4608, 0xdc74a589004377ab14836dce68ce2ca34e5b17147cd78ad4b3afe8137524ae8a),
|
||||
(3, 3, 0x8ebe9730c6cf61f56ef401d6f2bd229f3c298ca3c2791ee9137e4827b7af6c6d6566fca616eb1fe7adc2e4d56b5c7350ae3990c9905580630fa75ecffca8e001, 0xdc74a589004377ab14836dce68ce2ca34e5b17147cd78ad4b3afe8137524ae8a),
|
||||
(4, 5, 0x50cf418f7e217391e89ab9c2879ae68d7c7c597d846b4fe1c082b5b16e5d0c85c328fbf48ad3490bcfe94f446700ae0a4b0190e76d26cc752abced58f480c80f, 0xdc74a589004377ab14836dce68ce2ca34e5b17147cd78ad4b3afe8137524ae8a);
|
||||
|
||||
This Feature Branch:
|
||||
|
||||
|
||||
--
|
||||
-- Dumping data for table `state_user_transactions`
|
||||
--
|
||||
|
||||
INSERT INTO `state_user_transactions` (`id`, `state_user_id`, `transaction_id`, `transaction_type_id`, `balance`, `balance_date`) VALUES
|
||||
(1, 2, 1, 1, 10000000, '2021-11-05 00:25:46'),
|
||||
(12, 2, 7, 2, 9900000, '2021-11-05 00:55:37'),
|
||||
(13, 1, 7, 2, 100000, '2021-11-05 00:55:37'),
|
||||
(14, 2, 8, 2, 9800000, '2021-11-05 01:00:04'),
|
||||
(15, 1, 8, 2, 200000, '2021-11-05 01:00:04'),
|
||||
(16, 2, 10, 2, 9699772, '2021-11-05 01:17:41'),
|
||||
(17, 1, 10, 2, 299995, '2021-11-05 01:17:41');
|
||||
|
||||
--
|
||||
-- Dumping data for table `transactions`
|
||||
--
|
||||
|
||||
INSERT INTO `transactions` (`id`, `state_group_id`, `transaction_type_id`, `tx_hash`, `memo`, `received`, `blockchain_type_id`) VALUES
|
||||
(1, NULL, 1, 0xdd030d475479877587d927ed9024784ba62266cf1f3d87862fc98ad68f7b26e400000000000000000000000000000000, '', '2021-11-05 00:25:46', 1),
|
||||
(7, NULL, 2, NULL, 'Hier!', '2021-11-05 00:55:37', 1),
|
||||
(8, NULL, 2, NULL, 'Hier!', '2021-11-05 01:00:04', 1),
|
||||
(9, NULL, 9, 0xb1cbedbf126aa35f5edbf06e181c415361d05228ab4da9d19a4595285a673dfa00000000000000000000000000000000, '', '2021-11-05 01:05:34', 1),
|
||||
(10, NULL, 2, NULL, 'Hier!', '2021-11-05 01:17:41', 1);
|
||||
|
||||
--
|
||||
-- Dumping data for table `transaction_signatures`
|
||||
--
|
||||
|
||||
INSERT INTO `transaction_signatures` (`id`, `transaction_id`, `signature`, `pubkey`) VALUES
|
||||
(1, 1, 0x60d632479707e5d01cdc32c3326b5a5bae11173a0c06b719ee7b552f9fd644de1a0cd4afc207253329081d39dac1a63421f51571d836995c649fc39afac7480a, 0x48c45cb4fea925e83850f68f2fa8f27a1a4ed1bcba68cdb59fcd86adef3f52ee);
|
||||
*/
|
||||
|
||||
// Helper function
|
||||
async function calculateAndAddDecayTransactions(
|
||||
userTransactions: dbUserTransaction[],
|
||||
user: dbUser,
|
||||
decay: boolean,
|
||||
skipFirstTransaction: boolean,
|
||||
): Promise<Transaction[]> {
|
||||
const finalTransactions: Transaction[] = []
|
||||
const transactionIds: number[] = []
|
||||
const involvedUserIds: number[] = []
|
||||
|
||||
userTransactions.forEach((userTransaction: dbUserTransaction) => {
|
||||
transactionIds.push(userTransaction.transactionId)
|
||||
})
|
||||
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const transactions = await transactionRepository.joinFullTransactionsByIds(transactionIds)
|
||||
|
||||
const transactionIndiced: dbTransaction[] = []
|
||||
transactions.forEach((transaction: dbTransaction) => {
|
||||
transactionIndiced[transaction.id] = transaction
|
||||
if (transaction.transactionTypeId === TransactionTypeId.SEND) {
|
||||
involvedUserIds.push(transaction.transactionSendCoin.userId)
|
||||
involvedUserIds.push(transaction.transactionSendCoin.recipiantUserId)
|
||||
}
|
||||
})
|
||||
// remove duplicates
|
||||
// https://stackoverflow.com/questions/1960473/get-all-unique-values-in-a-javascript-array-remove-duplicates
|
||||
const involvedUsersUnique = involvedUserIds.filter((v, i, a) => a.indexOf(v) === i)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userIndiced = await userRepository.getUsersIndiced(involvedUsersUnique)
|
||||
|
||||
const decayStartTransaction = await transactionRepository.findDecayStartBlock()
|
||||
|
||||
for (let i = 0; i < userTransactions.length; i++) {
|
||||
const userTransaction = userTransactions[i]
|
||||
const transaction = transactionIndiced[userTransaction.transactionId]
|
||||
const finalTransaction = new Transaction()
|
||||
finalTransaction.transactionId = transaction.id
|
||||
finalTransaction.date = transaction.received.toISOString()
|
||||
finalTransaction.memo = transaction.memo
|
||||
finalTransaction.totalBalance = roundFloorFrom4(userTransaction.balance)
|
||||
const previousTransaction = i > 0 ? userTransactions[i - 1] : null
|
||||
|
||||
if (previousTransaction) {
|
||||
const currentTransaction = userTransaction
|
||||
const decay = await calculateDecayWithInterval(
|
||||
previousTransaction.balance,
|
||||
previousTransaction.balanceDate,
|
||||
currentTransaction.balanceDate,
|
||||
)
|
||||
const balance = previousTransaction.balance - decay.balance
|
||||
|
||||
if (
|
||||
decayStartTransaction &&
|
||||
decayStartTransaction.received < currentTransaction.balanceDate
|
||||
) {
|
||||
finalTransaction.decay = decay
|
||||
finalTransaction.decay.balance = roundFloorFrom4(balance)
|
||||
if (
|
||||
decayStartTransaction &&
|
||||
previousTransaction.transactionId < decayStartTransaction.id &&
|
||||
currentTransaction.transactionId > decayStartTransaction.id
|
||||
) {
|
||||
finalTransaction.decay.decayStartBlock = (
|
||||
decayStartTransaction.received.getTime() / 1000
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sender or receiver when user has sent money
|
||||
// group name if creation
|
||||
// type: gesendet / empfangen / geschöpft
|
||||
// transaktion nr / id
|
||||
// date
|
||||
// balance
|
||||
if (userTransaction.transactionTypeId === TransactionTypeId.CREATION) {
|
||||
// creation
|
||||
const creation = transaction.transactionCreation
|
||||
|
||||
finalTransaction.name = 'Gradido Akademie'
|
||||
finalTransaction.type = TransactionType.CREATION
|
||||
// finalTransaction.targetDate = creation.targetDate
|
||||
finalTransaction.balance = roundFloorFrom4(creation.amount)
|
||||
} else if (userTransaction.transactionTypeId === TransactionTypeId.SEND) {
|
||||
// send coin
|
||||
const sendCoin = transaction.transactionSendCoin
|
||||
let otherUser: dbUser | undefined
|
||||
finalTransaction.balance = roundFloorFrom4(sendCoin.amount)
|
||||
if (sendCoin.userId === user.id) {
|
||||
finalTransaction.type = TransactionType.SEND
|
||||
otherUser = userIndiced[sendCoin.recipiantUserId]
|
||||
// finalTransaction.pubkey = sendCoin.recipiantPublic
|
||||
} else if (sendCoin.recipiantUserId === user.id) {
|
||||
finalTransaction.type = TransactionType.RECIEVE
|
||||
otherUser = userIndiced[sendCoin.userId]
|
||||
// finalTransaction.pubkey = sendCoin.senderPublic
|
||||
} else {
|
||||
throw new Error('invalid transaction')
|
||||
}
|
||||
if (otherUser) {
|
||||
finalTransaction.name = otherUser.firstName + ' ' + otherUser.lastName
|
||||
finalTransaction.email = otherUser.email
|
||||
}
|
||||
}
|
||||
if (i > 0 || !skipFirstTransaction) {
|
||||
finalTransactions.push(finalTransaction)
|
||||
}
|
||||
|
||||
if (i === userTransactions.length - 1 && decay) {
|
||||
const now = new Date()
|
||||
const decay = await calculateDecayWithInterval(
|
||||
userTransaction.balance,
|
||||
userTransaction.balanceDate,
|
||||
now.getTime(),
|
||||
)
|
||||
const balance = userTransaction.balance - decay.balance
|
||||
|
||||
const decayTransaction = new Transaction()
|
||||
decayTransaction.type = 'decay'
|
||||
decayTransaction.balance = roundCeilFrom4(balance)
|
||||
decayTransaction.decayDuration = decay.decayDuration
|
||||
decayTransaction.decayStart = decay.decayStart
|
||||
decayTransaction.decayEnd = decay.decayEnd
|
||||
finalTransactions.push(decayTransaction)
|
||||
}
|
||||
}
|
||||
|
||||
return finalTransactions
|
||||
}
|
||||
|
||||
// Helper function
|
||||
async function listTransactions(
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
order: Order,
|
||||
user: dbUser,
|
||||
onlyCreations: boolean,
|
||||
): Promise<TransactionList> {
|
||||
let limit = pageSize
|
||||
let offset = 0
|
||||
let skipFirstTransaction = false
|
||||
if (currentPage > 1) {
|
||||
offset = (currentPage - 1) * pageSize - 1
|
||||
limit++
|
||||
}
|
||||
|
||||
if (offset && order === Order.ASC) {
|
||||
offset--
|
||||
}
|
||||
const userTransactionRepository = getCustomRepository(UserTransactionRepository)
|
||||
let [userTransactions, userTransactionsCount] = await userTransactionRepository.findByUserPaged(
|
||||
user.id,
|
||||
limit,
|
||||
offset,
|
||||
order,
|
||||
onlyCreations,
|
||||
)
|
||||
skipFirstTransaction = userTransactionsCount > offset + limit
|
||||
const decay = !(currentPage > 1)
|
||||
let transactions: Transaction[] = []
|
||||
if (userTransactions.length) {
|
||||
if (order === Order.DESC) {
|
||||
userTransactions = userTransactions.reverse()
|
||||
}
|
||||
transactions = await calculateAndAddDecayTransactions(
|
||||
userTransactions,
|
||||
user,
|
||||
decay,
|
||||
skipFirstTransaction,
|
||||
)
|
||||
if (order === Order.DESC) {
|
||||
transactions = transactions.reverse()
|
||||
}
|
||||
}
|
||||
|
||||
const transactionList = new TransactionList()
|
||||
transactionList.count = userTransactionsCount
|
||||
transactionList.transactions = transactions
|
||||
return transactionList
|
||||
}
|
||||
|
||||
// helper helper function
|
||||
async function updateStateBalance(
|
||||
user: dbUser,
|
||||
centAmount: number,
|
||||
received: Date,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<dbBalance> {
|
||||
const balanceRepository = getCustomRepository(BalanceRepository)
|
||||
let balance = await balanceRepository.findByUser(user.id)
|
||||
if (!balance) {
|
||||
balance = new dbBalance()
|
||||
balance.userId = user.id
|
||||
balance.amount = centAmount
|
||||
balance.modified = received
|
||||
} else {
|
||||
const decaiedBalance = await calculateDecay(balance.amount, balance.recordDate, received).catch(
|
||||
() => {
|
||||
throw new Error('error by calculating decay')
|
||||
},
|
||||
)
|
||||
balance.amount = Number(decaiedBalance) + centAmount
|
||||
balance.modified = new Date()
|
||||
}
|
||||
if (balance.amount <= 0) {
|
||||
throw new Error('error new balance <= 0')
|
||||
}
|
||||
balance.recordDate = received
|
||||
return queryRunner.manager.save(balance).catch((error) => {
|
||||
throw new Error('error saving balance:' + error)
|
||||
})
|
||||
}
|
||||
|
||||
// helper helper function
|
||||
async function addUserTransaction(
|
||||
user: dbUser,
|
||||
transaction: dbTransaction,
|
||||
centAmount: number,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<dbUserTransaction> {
|
||||
let newBalance = centAmount
|
||||
const userTransactionRepository = getCustomRepository(UserTransactionRepository)
|
||||
const lastUserTransaction = await userTransactionRepository.findLastForUser(user.id)
|
||||
if (lastUserTransaction) {
|
||||
newBalance += Number(
|
||||
await calculateDecay(
|
||||
Number(lastUserTransaction.balance),
|
||||
lastUserTransaction.balanceDate,
|
||||
transaction.received,
|
||||
).catch(() => {
|
||||
throw new Error('error by calculating decay')
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (newBalance <= 0) {
|
||||
throw new Error('error new balance <= 0')
|
||||
}
|
||||
|
||||
const newUserTransaction = new dbUserTransaction()
|
||||
newUserTransaction.userId = user.id
|
||||
newUserTransaction.transactionId = transaction.id
|
||||
newUserTransaction.transactionTypeId = transaction.transactionTypeId
|
||||
newUserTransaction.balance = newBalance
|
||||
newUserTransaction.balanceDate = transaction.received
|
||||
|
||||
return queryRunner.manager.save(newUserTransaction).catch((error) => {
|
||||
throw new Error('Error saving user transaction: ' + error)
|
||||
})
|
||||
}
|
||||
|
||||
async function getPublicKey(email: string): Promise<string | null> {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOne({ email: email })
|
||||
// User not found
|
||||
if (!loginUser) {
|
||||
return null
|
||||
}
|
||||
|
||||
return loginUser.pubKey.toString('hex')
|
||||
}
|
||||
import { User } from '../model/User'
|
||||
import { communityUser } from '../../util/communityUser'
|
||||
import { virtualDecayTransaction } from '../../util/virtualDecayTransaction'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { calculateDecay } from '../../util/decay'
|
||||
|
||||
@Resolver()
|
||||
export class TransactionResolver {
|
||||
@ -481,43 +48,92 @@ export class TransactionResolver {
|
||||
}: Paginated,
|
||||
@Ctx() context: any,
|
||||
): Promise<TransactionList> {
|
||||
// load user
|
||||
const now = new Date()
|
||||
// find user
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
let userEntity: dbUser | undefined
|
||||
if (userId) {
|
||||
userEntity = await userRepository.findOneOrFail({ id: userId })
|
||||
} else {
|
||||
userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
// TODO: separate those usecases - this is a security issue
|
||||
const user = userId
|
||||
? await userRepository.findOneOrFail({ id: userId }, { withDeleted: true })
|
||||
: await userRepository.findByPubkeyHex(context.pubKey)
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { balanceDate: 'DESC' } },
|
||||
)
|
||||
|
||||
// get GDT
|
||||
let balanceGDT = null
|
||||
try {
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: user.email,
|
||||
})
|
||||
if (!resultGDTSum.success) {
|
||||
throw new Error('Call not successful')
|
||||
}
|
||||
balanceGDT = Number(resultGDTSum.data.sum) || 0
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Could not query GDT Server', err)
|
||||
}
|
||||
|
||||
const transactions = await listTransactions(
|
||||
currentPage,
|
||||
if (!lastTransaction) {
|
||||
return new TransactionList(new Decimal(0), [], 0, balanceGDT)
|
||||
}
|
||||
|
||||
// find transactions
|
||||
// first page can contain 26 due to virtual decay transaction
|
||||
const offset = (currentPage - 1) * pageSize
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions, userTransactionsCount] = await transactionRepository.findByUserPaged(
|
||||
user.id,
|
||||
pageSize,
|
||||
offset,
|
||||
order,
|
||||
userEntity,
|
||||
onlyCreations,
|
||||
)
|
||||
|
||||
// get gdt sum
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: userEntity.email,
|
||||
// find involved users; I am involved
|
||||
const involvedUserIds: number[] = [user.id]
|
||||
userTransactions.forEach((transaction: dbTransaction) => {
|
||||
if (transaction.linkedUserId && !involvedUserIds.includes(transaction.linkedUserId)) {
|
||||
involvedUserIds.push(transaction.linkedUserId)
|
||||
}
|
||||
})
|
||||
if (!resultGDTSum.success) throw new Error(resultGDTSum.data)
|
||||
transactions.gdtSum = Number(resultGDTSum.data.sum / 100) || 0
|
||||
// We need to show the name for deleted users for old transactions
|
||||
const involvedDbUsers = await dbUser
|
||||
.createQueryBuilder()
|
||||
.withDeleted()
|
||||
.where('id IN (:...userIds)', { userIds: involvedUserIds })
|
||||
.getMany()
|
||||
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
||||
|
||||
// get balance
|
||||
const balanceRepository = getCustomRepository(BalanceRepository)
|
||||
const balanceEntity = await balanceRepository.findByUser(userEntity.id)
|
||||
if (balanceEntity) {
|
||||
const now = new Date()
|
||||
transactions.balance = roundFloorFrom4(balanceEntity.amount)
|
||||
transactions.decay = roundFloorFrom4(
|
||||
await calculateDecay(balanceEntity.amount, balanceEntity.recordDate, now),
|
||||
const self = new User(user)
|
||||
const transactions: Transaction[] = []
|
||||
|
||||
// decay transaction
|
||||
if (currentPage === 1 && order === Order.DESC) {
|
||||
transactions.push(
|
||||
virtualDecayTransaction(lastTransaction.balance, lastTransaction.balanceDate, now, self),
|
||||
)
|
||||
transactions.decayDate = now.toString()
|
||||
}
|
||||
|
||||
return transactions
|
||||
// transactions
|
||||
userTransactions.forEach((userTransaction) => {
|
||||
const linkedUser =
|
||||
userTransaction.typeId === TransactionTypeId.CREATION
|
||||
? communityUser
|
||||
: involvedUsers.find((u) => u.id === userTransaction.linkedUserId)
|
||||
transactions.push(new Transaction(userTransaction, self, linkedUser))
|
||||
})
|
||||
|
||||
// Construct Result
|
||||
return new TransactionList(
|
||||
calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now).balance,
|
||||
transactions,
|
||||
userTransactionsCount,
|
||||
balanceGDT,
|
||||
)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEND_COINS])
|
||||
@ -525,142 +141,92 @@ export class TransactionResolver {
|
||||
async sendCoins(
|
||||
@Args() { email, amount, memo }: TransactionSendArgs,
|
||||
@Ctx() context: any,
|
||||
): Promise<string> {
|
||||
): Promise<boolean> {
|
||||
// TODO this is subject to replay attacks
|
||||
// validate sender user (logged in)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const senderUser = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
if (senderUser.pubkey.length !== 32) {
|
||||
if (senderUser.pubKey.length !== 32) {
|
||||
throw new Error('invalid sender public key')
|
||||
}
|
||||
if (!hasUserAmount(senderUser, amount)) {
|
||||
throw new Error("user hasn't enough GDD")
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(senderUser.id, amount.mul(-1), receivedCallDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
// validate recipient user
|
||||
// TODO: the detour over the public key is unnecessary
|
||||
const recipiantPublicKey = await getPublicKey(email)
|
||||
if (!recipiantPublicKey) {
|
||||
throw new Error('recipiant not known')
|
||||
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
||||
if (!recipientUser) {
|
||||
throw new Error('recipient not known')
|
||||
}
|
||||
if (!isHexPublicKey(recipiantPublicKey)) {
|
||||
throw new Error('invalid recipiant public key')
|
||||
if (recipientUser.deletedAt) {
|
||||
throw new Error('The recipient account was deleted')
|
||||
}
|
||||
const recipiantUser = await userRepository.findByPubkeyHex(recipiantPublicKey)
|
||||
if (!recipiantUser) {
|
||||
throw new Error('Cannot find recipiant user by local send coins transaction')
|
||||
} else if (recipiantUser.disabled) {
|
||||
throw new Error('recipiant user account is disabled')
|
||||
if (!isHexPublicKey(recipientUser.pubKey.toString('hex'))) {
|
||||
throw new Error('invalid recipient public key')
|
||||
}
|
||||
|
||||
// validate amount
|
||||
if (amount <= 0) {
|
||||
throw new Error('invalid amount')
|
||||
}
|
||||
|
||||
const centAmount = Math.trunc(amount * 10000)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
try {
|
||||
// transaction
|
||||
let transaction = new dbTransaction()
|
||||
transaction.transactionTypeId = TransactionTypeId.SEND
|
||||
transaction.memo = memo
|
||||
const transactionSend = new dbTransaction()
|
||||
transactionSend.typeId = TransactionTypeId.SEND
|
||||
transactionSend.memo = memo
|
||||
transactionSend.userId = senderUser.id
|
||||
transactionSend.linkedUserId = recipientUser.id
|
||||
transactionSend.amount = amount.mul(-1)
|
||||
transactionSend.balance = sendBalance.balance
|
||||
transactionSend.balanceDate = receivedCallDate
|
||||
transactionSend.decay = sendBalance.decay.decay
|
||||
transactionSend.decayStart = sendBalance.decay.start
|
||||
transactionSend.previous = sendBalance.lastTransactionId
|
||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||
|
||||
// TODO: NO! this is problematic in its construction
|
||||
const insertResult = await queryRunner.manager.insert(dbTransaction, transaction)
|
||||
transaction = await queryRunner.manager
|
||||
.findOneOrFail(dbTransaction, insertResult.generatedMaps[0].id)
|
||||
.catch((error) => {
|
||||
throw new Error('error loading saved transaction: ' + error)
|
||||
})
|
||||
|
||||
// Insert Transaction: sender - amount
|
||||
const senderUserTransactionBalance = await addUserTransaction(
|
||||
senderUser,
|
||||
transaction,
|
||||
-centAmount,
|
||||
queryRunner,
|
||||
)
|
||||
// Insert Transaction: recipient + amount
|
||||
const recipiantUserTransactionBalance = await addUserTransaction(
|
||||
recipiantUser,
|
||||
transaction,
|
||||
centAmount,
|
||||
queryRunner,
|
||||
)
|
||||
|
||||
// Update Balance: sender - amount
|
||||
const senderStateBalance = await updateStateBalance(
|
||||
senderUser,
|
||||
-centAmount,
|
||||
transaction.received,
|
||||
queryRunner,
|
||||
)
|
||||
// Update Balance: recipiant + amount
|
||||
const recipiantStateBalance = await updateStateBalance(
|
||||
recipiantUser,
|
||||
centAmount,
|
||||
transaction.received,
|
||||
queryRunner,
|
||||
)
|
||||
|
||||
if (senderStateBalance.amount !== senderUserTransactionBalance.balance) {
|
||||
throw new Error('db data corrupted, sender')
|
||||
}
|
||||
if (recipiantStateBalance.amount !== recipiantUserTransactionBalance.balance) {
|
||||
throw new Error('db data corrupted, recipiant')
|
||||
const transactionReceive = new dbTransaction()
|
||||
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
||||
transactionReceive.memo = memo
|
||||
transactionReceive.userId = recipientUser.id
|
||||
transactionReceive.linkedUserId = senderUser.id
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipientUser.id, amount, receivedCallDate)
|
||||
if (!receiveBalance) {
|
||||
throw new Error('Sender user account corrupted')
|
||||
}
|
||||
transactionReceive.balance = receiveBalance.balance
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.decay = receiveBalance.decay.decay
|
||||
transactionReceive.decayStart = receiveBalance.decay.start
|
||||
transactionReceive.previous = receiveBalance.lastTransactionId
|
||||
transactionReceive.linkedTransactionId = transactionSend.id
|
||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||
|
||||
// transactionSendCoin
|
||||
const transactionSendCoin = new dbTransactionSendCoin()
|
||||
transactionSendCoin.transactionId = transaction.id
|
||||
transactionSendCoin.userId = senderUser.id
|
||||
transactionSendCoin.senderPublic = senderUser.pubkey
|
||||
transactionSendCoin.recipiantUserId = recipiantUser.id
|
||||
transactionSendCoin.recipiantPublic = Buffer.from(recipiantPublicKey, 'hex')
|
||||
transactionSendCoin.amount = centAmount
|
||||
transactionSendCoin.senderFinalBalance = senderStateBalance.amount
|
||||
await queryRunner.manager.save(transactionSendCoin).catch((error) => {
|
||||
throw new Error('error saving transaction send coin: ' + error)
|
||||
})
|
||||
|
||||
await queryRunner.manager.save(transaction).catch((error) => {
|
||||
throw new Error('error saving transaction with tx hash: ' + error)
|
||||
})
|
||||
// Save linked transaction id for send
|
||||
transactionSend.linkedTransactionId = transactionReceive.id
|
||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
// TODO: This is broken code - we should never correct an autoincrement index in production
|
||||
// according to dario it is required tho to properly work. The index of the table is used as
|
||||
// index for the transaction which requires a chain without gaps
|
||||
const count = await queryRunner.manager.count(dbTransaction)
|
||||
// fix autoincrement value which seems not effected from rollback
|
||||
await queryRunner
|
||||
.query('ALTER TABLE `transactions` auto_increment = ?', [count])
|
||||
.catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('problems with reset auto increment: %o', error)
|
||||
})
|
||||
throw e
|
||||
throw new Error(`Transaction was not successful: ${e}`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
// send notification email
|
||||
// TODO: translate
|
||||
await sendTransactionReceivedEmail({
|
||||
senderFirstName: senderUser.firstName,
|
||||
senderLastName: senderUser.lastName,
|
||||
recipientFirstName: recipiantUser.firstName,
|
||||
recipientLastName: recipiantUser.lastName,
|
||||
email: recipiantUser.email,
|
||||
recipientFirstName: recipientUser.firstName,
|
||||
recipientLastName: recipientUser.lastName,
|
||||
email: recipientUser.email,
|
||||
amount,
|
||||
memo,
|
||||
})
|
||||
|
||||
return 'success'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,14 +6,13 @@ import gql from 'graphql-tag'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import createServer from '../../server/createServer'
|
||||
import { resetDB, initialize } from '@dbTools/helpers'
|
||||
import { getRepository } from 'typeorm'
|
||||
import { LoginUser } from '@entity/LoginUser'
|
||||
import { LoginUserBackup } from '@entity/LoginUserBackup'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '../../config'
|
||||
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
|
||||
import { klicktippSignIn } from '../../apis/KlicktippController'
|
||||
// import { klicktippSignIn } from '../../apis/KlicktippController'
|
||||
|
||||
jest.setTimeout(10000)
|
||||
|
||||
jest.mock('../../mailer/sendAccountActivationEmail', () => {
|
||||
return {
|
||||
@ -22,12 +21,14 @@ jest.mock('../../mailer/sendAccountActivationEmail', () => {
|
||||
}
|
||||
})
|
||||
|
||||
/*
|
||||
jest.mock('../../apis/KlicktippController', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
klicktippSignIn: jest.fn(),
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
let mutate: any
|
||||
let con: any
|
||||
@ -40,6 +41,11 @@ beforeAll(async () => {
|
||||
await resetDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await resetDB(true)
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('UserResolver', () => {
|
||||
describe('createUser', () => {
|
||||
const variables = {
|
||||
@ -84,70 +90,32 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
let loginUser: LoginUser[]
|
||||
let user: User[]
|
||||
let loginUserBackup: LoginUserBackup[]
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
beforeAll(async () => {
|
||||
loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
|
||||
user = await getRepository(User).createQueryBuilder('state_user').getMany()
|
||||
loginUserBackup = await getRepository(LoginUserBackup)
|
||||
.createQueryBuilder('login_user_backup')
|
||||
.getMany()
|
||||
loginEmailOptIn = await getRepository(LoginEmailOptIn)
|
||||
.createQueryBuilder('login_email_optin')
|
||||
.getMany()
|
||||
user = await User.find()
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
})
|
||||
|
||||
describe('filling all tables', () => {
|
||||
it('saves the user in login_user table', () => {
|
||||
expect(loginUser).toEqual([
|
||||
expect(user).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
email: 'peter@lustig.de',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
username: '',
|
||||
description: '',
|
||||
password: '0',
|
||||
pubKey: null,
|
||||
privKey: null,
|
||||
emailHash: expect.any(Buffer),
|
||||
createdAt: expect.any(Date),
|
||||
emailChecked: false,
|
||||
passphraseShown: false,
|
||||
language: 'de',
|
||||
disabled: false,
|
||||
groupId: 1,
|
||||
publisherId: 1234,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('saves the user in state_user table', () => {
|
||||
expect(user).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
indexId: 0,
|
||||
groupId: 0,
|
||||
pubkey: expect.any(Buffer),
|
||||
email: 'peter@lustig.de',
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
username: '',
|
||||
disabled: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('saves the user in login_user_backup table', () => {
|
||||
expect(loginUserBackup).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
passphrase: expect.any(String),
|
||||
userId: loginUser[0].id,
|
||||
mnemonicType: 2,
|
||||
language: 'de',
|
||||
deletedAt: null,
|
||||
publisherId: 1234,
|
||||
},
|
||||
])
|
||||
})
|
||||
@ -156,7 +124,7 @@ describe('UserResolver', () => {
|
||||
expect(loginEmailOptIn).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
userId: loginUser[0].id,
|
||||
userId: user[0].id,
|
||||
verificationCode: expect.any(String),
|
||||
emailOptInTypeId: 1,
|
||||
createdAt: expect.any(Date),
|
||||
@ -196,9 +164,7 @@ describe('UserResolver', () => {
|
||||
mutation,
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' },
|
||||
})
|
||||
await expect(
|
||||
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
|
||||
).resolves.toEqual(
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
@ -215,9 +181,7 @@ describe('UserResolver', () => {
|
||||
mutation,
|
||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
||||
})
|
||||
await expect(
|
||||
getRepository(LoginUser).createQueryBuilder('login_user').getMany(),
|
||||
).resolves.toEqual(
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
@ -265,23 +229,17 @@ describe('UserResolver', () => {
|
||||
let emailOptIn: string
|
||||
|
||||
describe('valid optin code and valid password', () => {
|
||||
let loginUser: any
|
||||
let newLoginUser: any
|
||||
let newUser: any
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUserMutation, variables: createUserVariables })
|
||||
const loginEmailOptIn = await getRepository(LoginEmailOptIn)
|
||||
.createQueryBuilder('login_email_optin')
|
||||
.getMany()
|
||||
loginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
|
||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPasswordMutation,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
})
|
||||
newLoginUser = await getRepository(LoginUser).createQueryBuilder('login_user').getMany()
|
||||
newUser = await getRepository(User).createQueryBuilder('state_user').getMany()
|
||||
newUser = await User.find()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -289,38 +247,27 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('sets email checked to true', () => {
|
||||
expect(newLoginUser[0].emailChecked).toBeTruthy()
|
||||
expect(newUser[0].emailChecked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('updates the password', () => {
|
||||
expect(newLoginUser[0].password).toEqual('3917921995996627700')
|
||||
})
|
||||
|
||||
it('updates the public Key on both user tables', () => {
|
||||
expect(newLoginUser[0].pubKey).toEqual(expect.any(Buffer))
|
||||
expect(newLoginUser[0].pubKey).not.toEqual(loginUser[0].pubKey)
|
||||
expect(newLoginUser[0].pubKey).toEqual(newUser[0].pubkey)
|
||||
})
|
||||
|
||||
it('updates the private Key', () => {
|
||||
expect(newLoginUser[0].privKey).toEqual(expect.any(Buffer))
|
||||
expect(newLoginUser[0].privKey).not.toEqual(loginUser[0].privKey)
|
||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
||||
})
|
||||
|
||||
it('removes the optin', async () => {
|
||||
await expect(
|
||||
getRepository(LoginEmailOptIn).createQueryBuilder('login_email_optin').getMany(),
|
||||
).resolves.toHaveLength(0)
|
||||
await expect(LoginEmailOptIn.find()).resolves.toHaveLength(0)
|
||||
})
|
||||
|
||||
/*
|
||||
it('calls the klicktipp API', () => {
|
||||
expect(klicktippSignIn).toBeCalledWith(
|
||||
loginUser[0].email,
|
||||
loginUser[0].language,
|
||||
loginUser[0].firstName,
|
||||
loginUser[0].lastName,
|
||||
user[0].email,
|
||||
user[0].language,
|
||||
user[0].firstName,
|
||||
user[0].lastName,
|
||||
)
|
||||
})
|
||||
*/
|
||||
|
||||
it('returns true', () => {
|
||||
expect(result).toBeTruthy()
|
||||
@ -330,9 +277,7 @@ describe('UserResolver', () => {
|
||||
describe('no valid password', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUserMutation, variables: createUserVariables })
|
||||
const loginEmailOptIn = await getRepository(LoginEmailOptIn)
|
||||
.createQueryBuilder('login_email_optin')
|
||||
.getMany()
|
||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPasswordMutation,
|
||||
@ -380,8 +325,3 @@ describe('UserResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await resetDB(true)
|
||||
await con.close()
|
||||
})
|
||||
|
||||
@ -3,31 +3,26 @@
|
||||
|
||||
import fs from 'fs'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||
import { getConnection, getCustomRepository, getRepository, QueryRunner } from 'typeorm'
|
||||
import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm'
|
||||
import CONFIG from '../../config'
|
||||
import { User } from '../model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { encode } from '../../auth/JWT'
|
||||
import CheckUsernameArgs from '../arg/CheckUsernameArgs'
|
||||
import CreateUserArgs from '../arg/CreateUserArgs'
|
||||
import UnsecureLoginArgs from '../arg/UnsecureLoginArgs'
|
||||
import UpdateUserInfosArgs from '../arg/UpdateUserInfosArgs'
|
||||
import { klicktippNewsletterStateMiddleware } from '../../middleware/klicktippMiddleware'
|
||||
import { UserSettingRepository } from '../../typeorm/repository/UserSettingRepository'
|
||||
import { LoginUserRepository } from '../../typeorm/repository/LoginUser'
|
||||
import { Setting } from '../enum/Setting'
|
||||
import { UserRepository } from '../../typeorm/repository/User'
|
||||
import { LoginUser } from '@entity/LoginUser'
|
||||
import { LoginUserBackup } from '@entity/LoginUserBackup'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail'
|
||||
import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys'
|
||||
import { klicktippSignIn } from '../../apis/KlicktippController'
|
||||
import { RIGHTS } from '../../auth/RIGHTS'
|
||||
import { ServerUserRepository } from '../../typeorm/repository/ServerUser'
|
||||
import { ROLE_ADMIN } from '../../auth/ROLES'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { hasElopageBuys } from '../../util/hasElopageBuys'
|
||||
import { ServerUser } from '@entity/ServerUser'
|
||||
|
||||
const EMAIL_OPT_IN_RESET_PASSWORD = 2
|
||||
const EMAIL_OPT_IN_REGISTER = 1
|
||||
@ -49,7 +44,10 @@ const isLanguage = (language: string): boolean => {
|
||||
}
|
||||
|
||||
const PHRASE_WORD_COUNT = 24
|
||||
const WORDS = fs.readFileSync('src/config/mnemonic.english.txt').toString().split('\n')
|
||||
const WORDS = fs
|
||||
.readFileSync('src/config/mnemonic.uncompressed_buffer13116.txt')
|
||||
.toString()
|
||||
.split(',')
|
||||
const PassphraseGenerate = (): string[] => {
|
||||
const result = []
|
||||
for (let i = 0; i < PHRASE_WORD_COUNT; i++) {
|
||||
@ -154,8 +152,7 @@ const createEmailOptIn = async (
|
||||
loginUserId: number,
|
||||
queryRunner: QueryRunner,
|
||||
): Promise<LoginEmailOptIn> => {
|
||||
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
|
||||
let emailOptIn = await loginEmailOptInRepository.findOne({
|
||||
let emailOptIn = await LoginEmailOptIn.findOne({
|
||||
userId: loginUserId,
|
||||
emailOptInTypeId: EMAIL_OPT_IN_REGISTER,
|
||||
})
|
||||
@ -183,10 +180,9 @@ const createEmailOptIn = async (
|
||||
return emailOptIn
|
||||
}
|
||||
|
||||
const getOptInCode = async (loginUser: LoginUser): Promise<LoginEmailOptIn> => {
|
||||
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
|
||||
let optInCode = await loginEmailOptInRepository.findOne({
|
||||
userId: loginUser.id,
|
||||
const getOptInCode = async (loginUserId: number): Promise<LoginEmailOptIn> => {
|
||||
let optInCode = await LoginEmailOptIn.findOne({
|
||||
userId: loginUserId,
|
||||
emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD,
|
||||
})
|
||||
|
||||
@ -204,10 +200,10 @@ const getOptInCode = async (loginUser: LoginUser): Promise<LoginEmailOptIn> => {
|
||||
} else {
|
||||
optInCode = new LoginEmailOptIn()
|
||||
optInCode.verificationCode = random(64)
|
||||
optInCode.userId = loginUser.id
|
||||
optInCode.userId = loginUserId
|
||||
optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD
|
||||
}
|
||||
await loginEmailOptInRepository.save(optInCode)
|
||||
await LoginEmailOptIn.save(optInCode)
|
||||
return optInCode
|
||||
}
|
||||
|
||||
@ -220,18 +216,8 @@ export class UserResolver {
|
||||
// TODO refactor and do not have duplicate code with login(see below)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findByEmail(userEntity.email)
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = userEntity.email
|
||||
user.firstName = userEntity.firstName
|
||||
user.lastName = userEntity.lastName
|
||||
user.username = userEntity.username
|
||||
user.description = loginUser.description
|
||||
user.pubkey = userEntity.pubkey.toString('hex')
|
||||
user.language = loginUser.language
|
||||
|
||||
const user = new User(userEntity)
|
||||
// user.pubkey = userEntity.pubKey.toString('hex')
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage(context)
|
||||
|
||||
@ -256,89 +242,60 @@ export class UserResolver {
|
||||
@Ctx() context: any,
|
||||
): Promise<User> {
|
||||
email = email.trim().toLowerCase()
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findByEmail(email).catch(() => {
|
||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
||||
throw new Error('No user with this credentials')
|
||||
})
|
||||
if (!loginUser.emailChecked) {
|
||||
if (dbUser.deletedAt) {
|
||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
||||
}
|
||||
if (!dbUser.emailChecked) {
|
||||
throw new Error('User email not validated')
|
||||
}
|
||||
if (loginUser.password === BigInt(0)) {
|
||||
if (dbUser.password === BigInt(0)) {
|
||||
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||
throw new Error('User has no password set yet')
|
||||
}
|
||||
if (!loginUser.pubKey || !loginUser.privKey) {
|
||||
if (!dbUser.pubKey || !dbUser.privKey) {
|
||||
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||
throw new Error('User has no private or publicKey')
|
||||
}
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||
const loginUserPassword = BigInt(loginUser.password.toString())
|
||||
const loginUserPassword = BigInt(dbUser.password.toString())
|
||||
if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
|
||||
throw new Error('No user with this credentials')
|
||||
}
|
||||
// TODO: If user has no pubKey Create it again and update user.
|
||||
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
let userEntity: void | DbUser
|
||||
const loginUserPubKey = loginUser.pubKey
|
||||
const loginUserPubKeyString = loginUserPubKey.toString('hex')
|
||||
userEntity = await userRepository.findByPubkeyHex(loginUserPubKeyString).catch(() => {
|
||||
// User not stored in state_users
|
||||
// TODO: Check with production data - email is unique which can cause problems
|
||||
userEntity = new DbUser()
|
||||
userEntity.firstName = loginUser.firstName
|
||||
userEntity.lastName = loginUser.lastName
|
||||
userEntity.username = loginUser.username
|
||||
userEntity.email = loginUser.email
|
||||
userEntity.pubkey = loginUser.pubKey
|
||||
|
||||
userRepository.save(userEntity).catch(() => {
|
||||
throw new Error('error by save userEntity')
|
||||
})
|
||||
})
|
||||
if (!userEntity) {
|
||||
throw new Error('error with cannot happen')
|
||||
}
|
||||
|
||||
const user = new User()
|
||||
user.id = userEntity.id
|
||||
user.email = email
|
||||
user.firstName = loginUser.firstName
|
||||
user.lastName = loginUser.lastName
|
||||
user.username = loginUser.username
|
||||
user.description = loginUser.description
|
||||
user.pubkey = loginUserPubKeyString
|
||||
user.language = loginUser.language
|
||||
const user = new User(dbUser)
|
||||
// user.email = email
|
||||
// user.pubkey = dbUser.pubKey.toString('hex')
|
||||
user.language = dbUser.language
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage({ pubKey: loginUserPubKeyString })
|
||||
user.hasElopage = await this.hasElopage({ pubKey: dbUser.pubKey.toString('hex') })
|
||||
if (!user.hasElopage && publisherId) {
|
||||
user.publisherId = publisherId
|
||||
// TODO: Check if we can use updateUserInfos
|
||||
// await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey })
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
|
||||
loginUser.publisherId = publisherId
|
||||
loginUserRepository.save(loginUser)
|
||||
dbUser.publisherId = publisherId
|
||||
DbUser.save(dbUser)
|
||||
}
|
||||
|
||||
// coinAnimation
|
||||
const userSettingRepository = getCustomRepository(UserSettingRepository)
|
||||
const coinanimation = await userSettingRepository
|
||||
.readBoolean(userEntity.id, Setting.COIN_ANIMATION)
|
||||
.readBoolean(dbUser.id, Setting.COIN_ANIMATION)
|
||||
.catch((error) => {
|
||||
throw new Error(error)
|
||||
})
|
||||
user.coinanimation = coinanimation
|
||||
|
||||
// context.role is not set to the actual role yet on login
|
||||
const serverUserRepository = await getCustomRepository(ServerUserRepository)
|
||||
const countServerUsers = await serverUserRepository.count({ email: user.email })
|
||||
const countServerUsers = await ServerUser.count({ email: user.email })
|
||||
user.isAdmin = countServerUsers > 0
|
||||
|
||||
context.setHeaders.push({
|
||||
key: 'token',
|
||||
value: encode(loginUser.pubKey),
|
||||
value: encode(dbUser.pubKey),
|
||||
})
|
||||
|
||||
return user
|
||||
@ -368,18 +325,11 @@ export class UserResolver {
|
||||
language = DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
// Validate username
|
||||
// TODO: never true
|
||||
const username = ''
|
||||
if (username.length > 3 && !this.checkUsername({ username })) {
|
||||
throw new Error('Username already in use')
|
||||
}
|
||||
|
||||
// Validate email unique
|
||||
// TODO: i can register an email in upper/lower case twice
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const usersFound = await userRepository.count({ email })
|
||||
if (usersFound !== 0) {
|
||||
email = email.trim().toLowerCase()
|
||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||
if (userFound) {
|
||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||
throw new Error(`User already exists.`)
|
||||
}
|
||||
@ -390,18 +340,18 @@ export class UserResolver {
|
||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
const emailHash = getEmailHash(email)
|
||||
|
||||
// Table: login_users
|
||||
const loginUser = new LoginUser()
|
||||
loginUser.email = email
|
||||
loginUser.firstName = firstName
|
||||
loginUser.lastName = lastName
|
||||
loginUser.username = username
|
||||
loginUser.description = ''
|
||||
const dbUser = new DbUser()
|
||||
dbUser.email = email
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.emailHash = emailHash
|
||||
dbUser.language = language
|
||||
dbUser.publisherId = publisherId
|
||||
dbUser.passphrase = passphrase.join(' ')
|
||||
// TODO this field has no null allowed unlike the loginServer table
|
||||
// dbUser.pubKey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
|
||||
// dbUser.pubkey = keyPair[0]
|
||||
// loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||
loginUser.emailHash = emailHash
|
||||
loginUser.language = language
|
||||
loginUser.groupId = 1
|
||||
loginUser.publisherId = publisherId
|
||||
// loginUser.pubKey = keyPair[0]
|
||||
// loginUser.privKey = encryptedPrivkey
|
||||
|
||||
@ -409,43 +359,15 @@ export class UserResolver {
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
try {
|
||||
const { id: loginUserId } = await queryRunner.manager.save(loginUser).catch((error) => {
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('insert LoginUser failed', error)
|
||||
throw new Error('insert user failed')
|
||||
})
|
||||
|
||||
// Table: login_user_backups
|
||||
const loginUserBackup = new LoginUserBackup()
|
||||
loginUserBackup.userId = loginUserId
|
||||
loginUserBackup.passphrase = passphrase.join(' ') + ' ' // login server saves trailing space
|
||||
loginUserBackup.mnemonicType = 2 // ServerConfig::MNEMONIC_BIP0039_SORTED_ORDER;
|
||||
|
||||
await queryRunner.manager.save(loginUserBackup).catch((error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('insert LoginUserBackup failed', error)
|
||||
throw new Error('insert user backup failed')
|
||||
})
|
||||
|
||||
// Table: state_users
|
||||
const dbUser = new DbUser()
|
||||
dbUser.email = email
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.username = username
|
||||
// TODO this field has no null allowed unlike the loginServer table
|
||||
dbUser.pubkey = Buffer.from(randomBytes(32)) // Buffer.alloc(32, 0) default to 0000...
|
||||
// dbUser.pubkey = keyPair[0]
|
||||
|
||||
await queryRunner.manager.save(dbUser).catch((er) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error while saving dbUser', er)
|
||||
console.log('Error while saving dbUser', error)
|
||||
throw new Error('error saving user')
|
||||
})
|
||||
|
||||
// Store EmailOptIn in DB
|
||||
// TODO: this has duplicate code with sendResetPasswordEmail
|
||||
const emailOptIn = await createEmailOptIn(loginUserId, queryRunner)
|
||||
const emailOptIn = await createEmailOptIn(dbUser.id, queryRunner)
|
||||
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{code}/g,
|
||||
@ -473,17 +395,19 @@ export class UserResolver {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// THis is used by the admin only - should we move it to the admin resolver?
|
||||
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
|
||||
@Mutation(() => Boolean)
|
||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email: email })
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await DbUser.findOneOrFail({ email: email })
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
|
||||
try {
|
||||
const emailOptIn = await createEmailOptIn(loginUser.id, queryRunner)
|
||||
const emailOptIn = await createEmailOptIn(user.id, queryRunner)
|
||||
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{code}/g,
|
||||
@ -492,8 +416,8 @@ export class UserResolver {
|
||||
|
||||
const emailSent = await sendAccountActivationEmail({
|
||||
link: activationLink,
|
||||
firstName: loginUser.firstName,
|
||||
lastName: loginUser.lastName,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
})
|
||||
|
||||
@ -516,11 +440,10 @@ export class UserResolver {
|
||||
@Query(() => Boolean)
|
||||
async sendResetPasswordEmail(@Arg('email') email: string): Promise<boolean> {
|
||||
// TODO: this has duplicate code with createUser
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await DbUser.findOneOrFail({ email })
|
||||
|
||||
const loginUserRepository = await getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email })
|
||||
|
||||
const optInCode = await getOptInCode(loginUser)
|
||||
const optInCode = await getOptInCode(user.id)
|
||||
|
||||
const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace(
|
||||
/{code}/g,
|
||||
@ -529,8 +452,8 @@ export class UserResolver {
|
||||
|
||||
const emailSent = await sendResetPasswordEmail({
|
||||
link,
|
||||
firstName: loginUser.firstName,
|
||||
lastName: loginUser.lastName,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
})
|
||||
|
||||
@ -557,12 +480,9 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
// Load code
|
||||
const loginEmailOptInRepository = await getRepository(LoginEmailOptIn)
|
||||
const optInCode = await loginEmailOptInRepository
|
||||
.findOneOrFail({ verificationCode: code })
|
||||
.catch(() => {
|
||||
throw new Error('Could not login with emailVerificationCode')
|
||||
})
|
||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||
throw new Error('Could not login with emailVerificationCode')
|
||||
})
|
||||
|
||||
// Code is only valid for 10minutes
|
||||
const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime()
|
||||
@ -570,57 +490,43 @@ export class UserResolver {
|
||||
throw new Error('Code is older than 10 minutes')
|
||||
}
|
||||
|
||||
// load loginUser
|
||||
const loginUserRepository = await getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository
|
||||
.findOneOrFail({ id: optInCode.userId })
|
||||
.catch(() => {
|
||||
throw new Error('Could not find corresponding Login User')
|
||||
})
|
||||
|
||||
// load user
|
||||
const dbUserRepository = await getCustomRepository(UserRepository)
|
||||
const dbUser = await dbUserRepository.findOneOrFail({ email: loginUser.email }).catch(() => {
|
||||
throw new Error('Could not find corresponding User')
|
||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
||||
throw new Error('Could not find corresponding Login User')
|
||||
})
|
||||
|
||||
const loginUserBackupRepository = await getRepository(LoginUserBackup)
|
||||
const loginUserBackup = await loginUserBackupRepository
|
||||
.findOneOrFail({ userId: loginUser.id })
|
||||
.catch(() => {
|
||||
throw new Error('Could not find corresponding BackupUser')
|
||||
})
|
||||
// Generate Passphrase if needed
|
||||
if (!user.passphrase) {
|
||||
const passphrase = PassphraseGenerate()
|
||||
user.passphrase = passphrase.join(' ')
|
||||
}
|
||||
|
||||
const passphrase = loginUserBackup.passphrase.slice(0, -1).split(' ')
|
||||
const passphrase = user.passphrase.split(' ')
|
||||
if (passphrase.length < PHRASE_WORD_COUNT) {
|
||||
// TODO if this can happen we cannot recover from that
|
||||
// this seem to be good on production data, if we dont
|
||||
// make a coding mistake we do not have a problem here
|
||||
throw new Error('Could not load a correct passphrase')
|
||||
}
|
||||
|
||||
// Activate EMail
|
||||
loginUser.emailChecked = true
|
||||
user.emailChecked = true
|
||||
|
||||
// Update Password
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(loginUser.email, password) // return short and long hash
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash
|
||||
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
loginUser.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||
loginUser.pubKey = keyPair[0]
|
||||
loginUser.privKey = encryptedPrivkey
|
||||
dbUser.pubkey = keyPair[0]
|
||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||
user.pubKey = keyPair[0]
|
||||
user.privKey = encryptedPrivkey
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
|
||||
try {
|
||||
// Save loginUser
|
||||
await queryRunner.manager.save(loginUser).catch((error) => {
|
||||
throw new Error('error saving loginUser: ' + error)
|
||||
})
|
||||
|
||||
// Save user
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
await queryRunner.manager.save(user).catch((error) => {
|
||||
throw new Error('error saving user: ' + error)
|
||||
})
|
||||
|
||||
@ -641,12 +547,7 @@ export class UserResolver {
|
||||
// TODO do we always signUp the user? How to handle things with old users?
|
||||
if (optInCode.emailOptInTypeId === EMAIL_OPT_IN_REGISTER) {
|
||||
try {
|
||||
await klicktippSignIn(
|
||||
loginUser.email,
|
||||
loginUser.language,
|
||||
loginUser.firstName,
|
||||
loginUser.lastName,
|
||||
)
|
||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
||||
} catch {
|
||||
// TODO is this a problem?
|
||||
// eslint-disable-next-line no-console
|
||||
@ -664,8 +565,6 @@ export class UserResolver {
|
||||
{
|
||||
firstName,
|
||||
lastName,
|
||||
description,
|
||||
username,
|
||||
language,
|
||||
publisherId,
|
||||
password,
|
||||
@ -676,61 +575,42 @@ export class UserResolver {
|
||||
): Promise<boolean> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userEntity = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
const loginUserRepository = getCustomRepository(LoginUserRepository)
|
||||
const loginUser = await loginUserRepository.findOneOrFail({ email: userEntity.email })
|
||||
|
||||
if (username) {
|
||||
throw new Error('change username currently not supported!')
|
||||
// TODO: this error was thrown on login_server whenever you tried to change the username
|
||||
// to anything except "" which is an exception to the rules below. Those were defined
|
||||
// aswell, even tho never used.
|
||||
// ^[a-zA-Z][a-zA-Z0-9_-]*$
|
||||
// username must start with [a-z] or [A-Z] and than can contain also [0-9], - and _
|
||||
// username already used
|
||||
// userEntity.username = username
|
||||
}
|
||||
|
||||
if (firstName) {
|
||||
loginUser.firstName = firstName
|
||||
userEntity.firstName = firstName
|
||||
}
|
||||
|
||||
if (lastName) {
|
||||
loginUser.lastName = lastName
|
||||
userEntity.lastName = lastName
|
||||
}
|
||||
|
||||
if (description) {
|
||||
loginUser.description = description
|
||||
}
|
||||
|
||||
if (language) {
|
||||
if (!isLanguage(language)) {
|
||||
throw new Error(`"${language}" isn't a valid language`)
|
||||
}
|
||||
loginUser.language = language
|
||||
userEntity.language = language
|
||||
}
|
||||
|
||||
if (password && passwordNew) {
|
||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, password)
|
||||
if (BigInt(loginUser.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||
throw new Error(`Old password is invalid`)
|
||||
}
|
||||
|
||||
const privKey = SecretKeyCryptographyDecrypt(loginUser.privKey, oldPasswordHash[1])
|
||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||
|
||||
const newPasswordHash = SecretKeyCryptographyCreateKey(loginUser.email, passwordNew) // return short and long hash
|
||||
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||
|
||||
// Save new password hash and newly encrypted private key
|
||||
loginUser.password = newPasswordHash[0].readBigUInt64LE()
|
||||
loginUser.privKey = encryptedPrivkey
|
||||
userEntity.password = newPasswordHash[0].readBigUInt64LE()
|
||||
userEntity.privKey = encryptedPrivkey
|
||||
}
|
||||
|
||||
// Save publisherId only if Elopage is not yet registered
|
||||
if (publisherId && !(await this.hasElopage(context))) {
|
||||
loginUser.publisherId = publisherId
|
||||
userEntity.publisherId = publisherId
|
||||
}
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
@ -747,10 +627,6 @@ export class UserResolver {
|
||||
})
|
||||
}
|
||||
|
||||
await queryRunner.manager.save(loginUser).catch((error) => {
|
||||
throw new Error('error saving loginUser: ' + error)
|
||||
})
|
||||
|
||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||
throw new Error('error saving user: ' + error)
|
||||
})
|
||||
@ -766,30 +642,6 @@ export class UserResolver {
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CHECK_USERNAME])
|
||||
@Query(() => Boolean)
|
||||
async checkUsername(@Args() { username }: CheckUsernameArgs): Promise<boolean> {
|
||||
// Username empty?
|
||||
if (username === '') {
|
||||
throw new Error('Username must be set.')
|
||||
}
|
||||
|
||||
// Do we fullfil the minimum character length?
|
||||
const MIN_CHARACTERS_USERNAME = 2
|
||||
if (username.length < MIN_CHARACTERS_USERNAME) {
|
||||
throw new Error(`Username must be at minimum ${MIN_CHARACTERS_USERNAME} characters long.`)
|
||||
}
|
||||
|
||||
const usersFound = await LoginUser.count({ username })
|
||||
|
||||
// Username already present?
|
||||
if (usersFound !== 0) {
|
||||
throw new Error(`Username "${username}" already taken.`)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.HAS_ELOPAGE])
|
||||
@Query(() => Boolean)
|
||||
async hasElopage(@Ctx() context: any): Promise<boolean> {
|
||||
@ -799,8 +651,6 @@ export class UserResolver {
|
||||
return false
|
||||
}
|
||||
|
||||
const loginElopageBuysRepository = getCustomRepository(LoginElopageBuysRepository)
|
||||
const elopageBuyCount = await loginElopageBuysRepository.count({ payerEmail: userEntity.email })
|
||||
return elopageBuyCount > 0
|
||||
return hasElopageBuys(userEntity.email)
|
||||
}
|
||||
}
|
||||
|
||||
23
backend/src/graphql/scalar/Decimal.ts
Normal file
23
backend/src/graphql/scalar/Decimal.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { GraphQLScalarType, Kind } from 'graphql'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export default new GraphQLScalarType({
|
||||
name: 'Decimal',
|
||||
description: 'The `Decimal` scalar type to represent currency values',
|
||||
|
||||
serialize(value: Decimal) {
|
||||
return value.toString()
|
||||
},
|
||||
|
||||
parseValue(value) {
|
||||
return new Decimal(value)
|
||||
},
|
||||
|
||||
parseLiteral(ast) {
|
||||
if (ast.kind !== Kind.STRING) {
|
||||
throw new TypeError(`${String(ast)} is not a valid decimal value.`)
|
||||
}
|
||||
|
||||
return new Decimal(ast.value)
|
||||
},
|
||||
})
|
||||
@ -3,11 +3,14 @@ import { buildSchema } from 'type-graphql'
|
||||
import path from 'path'
|
||||
|
||||
import isAuthorized from './directive/isAuthorized'
|
||||
import DecimalScalar from './scalar/Decimal'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
const schema = async (): Promise<GraphQLSchema> => {
|
||||
return buildSchema({
|
||||
resolvers: [path.join(__dirname, 'resolver', `!(*.test).{js,ts}`)],
|
||||
authChecker: isAuthorized,
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
@ -16,7 +17,7 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
recipientFirstName: 'Peter',
|
||||
recipientLastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
amount: 42.0,
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionReceived } from './text/transactionReceived'
|
||||
|
||||
@ -7,7 +8,7 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
amount: number
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): Promise<boolean> => {
|
||||
return sendEMail({
|
||||
|
||||
@ -4,7 +4,7 @@ export const accountActivation = {
|
||||
text: (data: { link: string; firstName: string; lastName: string; email: string }): string =>
|
||||
`Hallo ${data.firstName} ${data.lastName},
|
||||
|
||||
Deine EMail wurde soeben bei Gradido registriert.
|
||||
Deine E-Mail-Adresse wurde soeben bei Gradido registriert.
|
||||
|
||||
Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:
|
||||
${data.link}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const transactionReceived = {
|
||||
de: {
|
||||
subject: 'Gradido Überweisung',
|
||||
@ -7,7 +9,7 @@ export const transactionReceived = {
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
email: string
|
||||
amount: number
|
||||
amount: Decimal
|
||||
memo: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import 'reflect-metadata'
|
||||
import 'module-alias/register'
|
||||
|
||||
import { ApolloServer } from 'apollo-server-express'
|
||||
import express from 'express'
|
||||
import express, { Express } from 'express'
|
||||
|
||||
// database
|
||||
import connection from '../typeorm/connection'
|
||||
import getDBVersion from '../typeorm/getDBVersion'
|
||||
import { checkDBVersion } from '../typeorm/DBVersion'
|
||||
|
||||
// server
|
||||
import cors from './cors'
|
||||
@ -24,27 +21,25 @@ import schema from '../graphql/schema'
|
||||
|
||||
// webhooks
|
||||
import { elopageWebhook } from '../webhook/elopage'
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
|
||||
// TODO implement
|
||||
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
|
||||
|
||||
const DB_VERSION = '0006-login_users_collation'
|
||||
type ServerDef = { apollo: ApolloServer; app: Express; con: Connection }
|
||||
|
||||
const createServer = async (context: any = serverContext): Promise<any> => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const createServer = async (context: any = serverContext): Promise<ServerDef> => {
|
||||
// open mysql connection
|
||||
const con = await connection()
|
||||
if (!con || !con.isConnected) {
|
||||
throw new Error(`Couldn't open connection to database`)
|
||||
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||
}
|
||||
|
||||
// check for correct database version
|
||||
const dbVersion = await getDBVersion()
|
||||
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||
throw new Error(
|
||||
`Wrong database version - the backend requires '${DB_VERSION}' but found '${
|
||||
dbVersion || 'None'
|
||||
}'`,
|
||||
)
|
||||
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||
if (!dbVersion) {
|
||||
throw new Error('Fatal: Database Version incorrect')
|
||||
}
|
||||
|
||||
// Express Server
|
||||
@ -55,17 +50,8 @@ const createServer = async (context: any = serverContext): Promise<any> => {
|
||||
|
||||
// bodyparser json
|
||||
app.use(express.json())
|
||||
// bodyparser text for elopage
|
||||
app.use(express.text())
|
||||
|
||||
// Log every request
|
||||
/*
|
||||
app.use((req, res, next) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(req)
|
||||
next()
|
||||
})
|
||||
*/
|
||||
// bodyparser urlencoded for elopage
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
||||
// Elopage Webhook
|
||||
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
|
||||
|
||||
@ -1,27 +1,45 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { ApolloLogPlugin } from 'apollo-log'
|
||||
import { ApolloLogPlugin, LogMutateData } from 'apollo-log'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
const plugins = [
|
||||
{
|
||||
requestDidStart() {
|
||||
return {
|
||||
willSendResponse(requestContext: any) {
|
||||
const { setHeaders = [] } = requestContext.context
|
||||
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
|
||||
if (requestContext.response.http.headers.get(key)) {
|
||||
requestContext.response.http.headers.set(key, value)
|
||||
} else {
|
||||
requestContext.response.http.headers.append(key, value)
|
||||
}
|
||||
})
|
||||
return requestContext
|
||||
},
|
||||
}
|
||||
},
|
||||
const setHeadersPlugin = {
|
||||
requestDidStart() {
|
||||
return {
|
||||
willSendResponse(requestContext: any) {
|
||||
const { setHeaders = [] } = requestContext.context
|
||||
setHeaders.forEach(({ key, value }: { [key: string]: string }) => {
|
||||
if (requestContext.response.http.headers.get(key)) {
|
||||
requestContext.response.http.headers.set(key, value)
|
||||
} else {
|
||||
requestContext.response.http.headers.append(key, value)
|
||||
}
|
||||
})
|
||||
return requestContext
|
||||
},
|
||||
}
|
||||
},
|
||||
ApolloLogPlugin(),
|
||||
]
|
||||
}
|
||||
|
||||
const apolloLogPlugin = ApolloLogPlugin({
|
||||
mutate: (data: LogMutateData) => {
|
||||
// We need to deep clone the object in order to not modify the actual request
|
||||
const dataCopy = cloneDeep(data)
|
||||
|
||||
// mask password if part of the query
|
||||
if (dataCopy.context.request.variables && dataCopy.context.request.variables.password) {
|
||||
dataCopy.context.request.variables.password = '***'
|
||||
}
|
||||
|
||||
// mask token at all times
|
||||
dataCopy.context.context.token = '***'
|
||||
|
||||
return dataCopy
|
||||
},
|
||||
})
|
||||
|
||||
const plugins =
|
||||
process.env.NODE_ENV === 'development' ? [setHeadersPlugin] : [setHeadersPlugin, apolloLogPlugin]
|
||||
|
||||
export default plugins
|
||||
|
||||
28
backend/src/typeorm/DBVersion.ts
Normal file
28
backend/src/typeorm/DBVersion.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Migration } from '@entity/Migration'
|
||||
|
||||
const getDBVersion = async (): Promise<string | null> => {
|
||||
try {
|
||||
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
||||
return dbVersion ? dbVersion.fileName : null
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||
const dbVersion = await getDBVersion()
|
||||
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`Wrong database version detected - the backend requires '${DB_VERSION}' but found '${
|
||||
dbVersion || 'None'
|
||||
}`,
|
||||
)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export { checkDBVersion, getDBVersion }
|
||||
@ -1,11 +1,12 @@
|
||||
import { createConnection, Connection, FileLogger } from 'typeorm'
|
||||
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||
// We cannot use our connection here, but must use the external typeorm installation
|
||||
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||
import CONFIG from '../config'
|
||||
import { entities } from '@entity/index'
|
||||
|
||||
const connection = async (): Promise<Connection | null> => {
|
||||
let con = null
|
||||
try {
|
||||
con = await createConnection({
|
||||
return createConnection({
|
||||
name: 'default',
|
||||
type: 'mysql',
|
||||
host: CONFIG.DB_HOST,
|
||||
@ -23,9 +24,8 @@ const connection = async (): Promise<Connection | null> => {
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return null
|
||||
}
|
||||
|
||||
return con
|
||||
}
|
||||
|
||||
export default connection
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
import { getRepository } from 'typeorm'
|
||||
import { Migration } from '@entity/Migration'
|
||||
|
||||
const getDBVersion = async (): Promise<string | null> => {
|
||||
try {
|
||||
const dbVersion = await getRepository(Migration).findOne({ order: { version: 'DESC' } })
|
||||
return dbVersion ? dbVersion.fileName : null
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export default getDBVersion
|
||||
@ -1,9 +0,0 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { Balance } from '@entity/Balance'
|
||||
|
||||
@EntityRepository(Balance)
|
||||
export class BalanceRepository extends Repository<Balance> {
|
||||
findByUser(userId: number): Promise<Balance | undefined> {
|
||||
return this.createQueryBuilder('balance').where('balance.userId = :userId', { userId }).getOne()
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
||||
|
||||
@EntityRepository(LoginElopageBuys)
|
||||
export class LoginElopageBuysRepository extends Repository<LoginElopageBuys> {}
|
||||
@ -1,5 +0,0 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
|
||||
@EntityRepository(LoginEmailOptIn)
|
||||
export class LoginEmailOptInRepository extends Repository<LoginEmailOptIn> {}
|
||||
@ -1,5 +0,0 @@
|
||||
import { EntityRepository, Repository } from 'typeorm'
|
||||
import { LoginPendingTasksAdmin } from '@entity/LoginPendingTasksAdmin'
|
||||
|
||||
@EntityRepository(LoginPendingTasksAdmin)
|
||||
export class LoginPendingTasksAdminRepository extends Repository<LoginPendingTasksAdmin> {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user