Merge branch 'master' into 1170_Send_new_Users_Gradido

This commit is contained in:
Ulf Gebhardt 2022-03-07 13:14:08 +01:00 committed by GitHub
commit 3e3b5871a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1332 changed files with 11245 additions and 160268 deletions

View File

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

View File

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

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

View File

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

View File

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

@ -2,7 +2,8 @@ node_modules/
dist/
.cache/
.env
/.env
/.env.bak
# coverage folder
coverage/

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -13,3 +13,11 @@ export default {
components: { defaultLayout },
}
</script>
<style>
.pointer {
cursor: pointer;
}
.pointer:hover {
background-color: rgb(216, 213, 213);
}
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const deleteUser = gql`
mutation ($userId: Float!) {
deleteUser(userId: $userId)
}
`

View File

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

View File

@ -0,0 +1,7 @@
import gql from 'graphql-tag'
export const unDeleteUser = gql`
mutation ($userId: Float!) {
unDeleteUser(userId: $userId)
}
`

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@ -1,5 +1,6 @@
/node_modules/
/.env
/.env.bak
/build/
package-json.lock
coverage

View File

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

View File

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

View File

@ -7,5 +7,4 @@ export const INALIENABLE_RIGHTS = [
RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL,
RIGHTS.SET_PASSWORD,
RIGHTS.CHECK_USERNAME,
]

View File

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

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

View File

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

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

View File

@ -1,7 +0,0 @@
import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class CheckUsernameArgs {
@Field(() => String)
username: string
}

View File

@ -13,4 +13,7 @@ export default class SearchUsersArgs {
@Field(() => Boolean, { nullable: true })
notActivated?: boolean
@Field(() => Boolean, { nullable: true })
isDeleted?: boolean
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
@EntityRepository(LoginElopageBuys)
export class LoginElopageBuysRepository extends Repository<LoginElopageBuys> {}

View File

@ -1,5 +0,0 @@
import { EntityRepository, Repository } from 'typeorm'
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
@EntityRepository(LoginEmailOptIn)
export class LoginEmailOptInRepository extends Repository<LoginEmailOptIn> {}

View File

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