mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into fix_clear_yarn_cache
This commit is contained in:
commit
f16b7fe391
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@ -0,0 +1,3 @@
|
||||
**/node_modules
|
||||
**/build
|
||||
**/coverage
|
||||
20
.github/workflows/publish.yml
vendored
20
.github/workflows/publish.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
||||
docker build --target production -t "gradido/frontend:latest" -t "gradido/frontend:production" -t "gradido/frontend:${VERSION}" -t "gradido/frontend:${BUILD_VERSION}" frontend/
|
||||
docker save "gradido/frontend" > /tmp/frontend.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-frontend-production
|
||||
path: /tmp/frontend.tar
|
||||
@ -75,7 +75,7 @@ jobs:
|
||||
docker build -f ./backend/Dockerfile --target production -t "gradido/backend:latest" -t "gradido/backend:production" -t "gradido/backend:${VERSION}" -t "gradido/backend:${BUILD_VERSION}" .
|
||||
docker save "gradido/backend" > /tmp/backend.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-backend-production
|
||||
path: /tmp/backend.tar
|
||||
@ -101,7 +101,7 @@ jobs:
|
||||
docker build --target production_up -t "gradido/database:production_up" database/
|
||||
docker save "gradido/database:production_up" > /tmp/database_up.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-database-production_up
|
||||
path: /tmp/database_up.tar
|
||||
@ -138,7 +138,7 @@ jobs:
|
||||
docker build -t "gradido/mariadb:latest" -t "gradido/mariadb:production" -t "gradido/mariadb:${VERSION}" -t "gradido/mariadb:${BUILD_VERSION}" -f ./mariadb/Dockerfile ./
|
||||
docker save "gradido/mariadb" > /tmp/mariadb.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-mariadb-production
|
||||
path: /tmp/mariadb.tar
|
||||
@ -175,7 +175,7 @@ jobs:
|
||||
docker build -t "gradido/nginx:latest" -t "gradido/nginx:production" -t "gradido/nginx:${VERSION}" -t "gradido/nginx:${BUILD_VERSION}" nginx/
|
||||
docker save "gradido/nginx" > /tmp/nginx.tar
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-nginx-production
|
||||
path: /tmp/nginx.tar
|
||||
@ -200,35 +200,35 @@ jobs:
|
||||
# DOWNLOAD DOCKER IMAGES #################################################
|
||||
##########################################################################
|
||||
- name: Download Docker Image (Frontend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-frontend-production
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/frontend.tar
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-backend-production
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/backend.tar
|
||||
- name: Download Docker Image (Database)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-database-production_up
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/database_up.tar
|
||||
- name: Download Docker Image (MariaDB)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-mariadb-production
|
||||
path: /tmp
|
||||
- name: Load Docker Image
|
||||
run: docker load < /tmp/mariadb.tar
|
||||
- name: Download Docker Image (Nginx)
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-nginx-production
|
||||
path: /tmp
|
||||
|
||||
2
.github/workflows/test_admin_interface.yml
vendored
2
.github/workflows/test_admin_interface.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Admin Interface | Build 'test' image
|
||||
run: docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
|
||||
run: docker build -f ./admin/Dockerfile --target test -t "gradido/admin:test" --build-arg NODE_ENV="test" .
|
||||
|
||||
unit_test:
|
||||
if: needs.files-changed.outputs.admin == 'true'
|
||||
|
||||
4
.github/workflows/test_backend.yml
vendored
4
.github/workflows/test_backend.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||
|
||||
- name: Backend | Unit tests
|
||||
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
|
||||
run: cd database && yarn && yarn build && cd ../config && yarn install && cd ../backend && yarn && yarn test
|
||||
|
||||
lint:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
@ -66,7 +66,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Backend | Lint
|
||||
run: cd database && yarn && cd ../backend && yarn && yarn run lint
|
||||
run: cd database && yarn && cd ../config && yarn install && cd ../backend && yarn && yarn run lint
|
||||
|
||||
locales:
|
||||
if: needs.files-changed.outputs.backend == 'true'
|
||||
|
||||
6
.github/workflows/test_dht_node.yml
vendored
6
.github/workflows/test_dht_node.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
docker save "gradido/dht-node:test" > /tmp/dht-node.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-dht-node-test
|
||||
path: /tmp/dht-node.tar
|
||||
@ -51,7 +51,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Lint
|
||||
run: cd database && yarn && cd ../dht-node && yarn && yarn run lint
|
||||
run: cd database && yarn && cd ../config && yarn install && cd ../dht-node && yarn && yarn run lint
|
||||
|
||||
unit_test:
|
||||
name: Unit Tests - DHT Node
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-dht-node-test
|
||||
path: /tmp
|
||||
|
||||
2
.github/workflows/test_dlt_connector.yml
vendored
2
.github/workflows/test_dlt_connector.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
docker save "gradido/dlt-connector:test" > /tmp/dlt-connector.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-dlt-connector-test
|
||||
path: /tmp/dlt-connector.tar
|
||||
|
||||
27
.github/workflows/test_e2e.yml
vendored
27
.github/workflows/test_e2e.yml
vendored
@ -5,7 +5,7 @@ on: push
|
||||
jobs:
|
||||
end-to-end-tests:
|
||||
name: End-to-End Tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
@ -13,16 +13,6 @@ jobs:
|
||||
- name: Boot up test system | docker-compose mariadb
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
|
||||
|
||||
- name: Boot up test system | docker-compose database
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||
|
||||
- name: Boot up test system | docker-compose backend
|
||||
run: |
|
||||
cd backend
|
||||
cp .env.test_e2e .env
|
||||
cd ..
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
|
||||
|
||||
- name: Sleep for 10 seconds
|
||||
run: sleep 10s
|
||||
|
||||
@ -31,14 +21,17 @@ jobs:
|
||||
sudo chown runner:docker -R *
|
||||
cd database
|
||||
yarn && yarn dev_reset
|
||||
cd ../config
|
||||
yarn install
|
||||
cd ../backend
|
||||
yarn && yarn seed
|
||||
|
||||
- name: Boot up test system | docker-compose frontends
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
|
||||
|
||||
- name: Boot up test system | docker-compose mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
|
||||
- name: Boot up test system | docker-compose backend, frontend, admin, nginx, mailserver
|
||||
run: |
|
||||
cd backend
|
||||
cp .env.test_e2e .env
|
||||
cd ..
|
||||
docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend frontend admin nginx mailserver
|
||||
|
||||
- name: End-to-end tests | prepare
|
||||
run: |
|
||||
@ -68,7 +61,7 @@ jobs:
|
||||
- name: End-to-end tests | if tests failed, upload report
|
||||
id: e2e-report
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cypress-report-pr-#${{ steps.pr.outputs.number }}
|
||||
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/reports/cucumber_html_report
|
||||
|
||||
4
.github/workflows/test_federation.yml
vendored
4
.github/workflows/test_federation.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
docker save "gradido/federation:test" > /tmp/federation.tar
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docker-federation-test
|
||||
path: /tmp/federation.tar
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download Docker Image
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: docker-federation-test
|
||||
path: /tmp
|
||||
|
||||
2
.github/workflows/test_frontend.yml
vendored
2
.github/workflows/test_frontend.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Frontend | Build 'test' image
|
||||
run: docker build --target test -t "gradido/frontend:test" frontend/ --build-arg NODE_ENV="test"
|
||||
run: docker build -f ./frontend/Dockerfile --target test -t "gradido/frontend:test" --build-arg NODE_ENV="test" .
|
||||
|
||||
unit_test:
|
||||
if: needs.files-changed.outputs.frontend == 'true'
|
||||
|
||||
32
.github/workflows/test_mariadb.yml
vendored
32
.github/workflows/test_mariadb.yml
vendored
@ -1,32 +0,0 @@
|
||||
name: Gradido MariaDB Test CI
|
||||
|
||||
on: push
|
||||
|
||||
jobs:
|
||||
files-changed:
|
||||
name: Detect File Changes - MariaDB
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
mariadb: ${{ steps.changes.outputs.mariadb }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Check for frontend file changes
|
||||
uses: dorny/paths-filter@v2.11.1
|
||||
id: changes
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
filters: .github/file-filters.yml
|
||||
list-files: shell
|
||||
|
||||
build_test:
|
||||
if: needs.files-changed.outputs.mariadb == 'true'
|
||||
name: Docker Build Test - MariaDB
|
||||
needs: files-changed
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: MariaDB | Build 'test' image
|
||||
run: docker build --target mariadb_server -t "gradido/mariadb:test" -f ./mariadb/Dockerfile ./
|
||||
87
CHANGELOG.md
87
CHANGELOG.md
@ -4,8 +4,95 @@ 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).
|
||||
|
||||
#### [2.4.5](https://github.com/gradido/gradido/compare/2.3.1...2.4.5)
|
||||
|
||||
- feat(backend): add answer button inside E-Mail [`#3431`](https://github.com/gradido/gradido/pull/3431)
|
||||
- feat(other): build config in deployment [`#3430`](https://github.com/gradido/gradido/pull/3430)
|
||||
- fix(dht): and federation using config in bare-metal setup [`#3434`](https://github.com/gradido/gradido/pull/3434)
|
||||
- fix(frontend): fix meta url from env.template [`#3432`](https://github.com/gradido/gradido/pull/3432)
|
||||
- fix(frontend): fix problems with dev container [`#3429`](https://github.com/gradido/gradido/pull/3429)
|
||||
- feat(backend): add config validation with joi [`#3422`](https://github.com/gradido/gradido/pull/3422)
|
||||
- feat(backend): patch getUsername in PublishNameLogic [`#3428`](https://github.com/gradido/gradido/pull/3428)
|
||||
- feat(admin): resubmission date localized [`#3426`](https://github.com/gradido/gradido/pull/3426)
|
||||
- refactor(workflow): increaste artifact version [`#3425`](https://github.com/gradido/gradido/pull/3425)
|
||||
- feat(backend): make sure, correct user name is used for jwt token [`#3424`](https://github.com/gradido/gradido/pull/3424)
|
||||
- feat(frontend): speedup frontend build time [`#3423`](https://github.com/gradido/gradido/pull/3423)
|
||||
- chore(release): v2.4.4 beta [`#3421`](https://github.com/gradido/gradido/pull/3421)
|
||||
- fix(admin): copy of env variables [`#3420`](https://github.com/gradido/gradido/pull/3420)
|
||||
- fix(backend): toggle for using worker threads for password encryption [`#3418`](https://github.com/gradido/gradido/pull/3418)
|
||||
- fix(admin): fix resubmission [`#3416`](https://github.com/gradido/gradido/pull/3416)
|
||||
- feat(backend): run password encryption in worker thread [`#3410`](https://github.com/gradido/gradido/pull/3410)
|
||||
- feat(frontend): user location with vue3 in cooperation with montreal [`#3397`](https://github.com/gradido/gradido/pull/3397)
|
||||
- feat(backend): optimize find user query on login [`#3415`](https://github.com/gradido/gradido/pull/3415)
|
||||
- feat(frontend): show gdt comments [`#3412`](https://github.com/gradido/gradido/pull/3412)
|
||||
- fix(backend): humhub use first name instead of username field for initalien based alias [`#3403`](https://github.com/gradido/gradido/pull/3403)
|
||||
- fix(frontend): repair gdt transaction list pagination [`#3409`](https://github.com/gradido/gradido/pull/3409)
|
||||
- fix(frontend): updated tests [`#3406`](https://github.com/gradido/gradido/pull/3406)
|
||||
- feat(backend): speed up gdt request with keep alive connections [`#3392`](https://github.com/gradido/gradido/pull/3392)
|
||||
- fix(frontend): fix contribution data handling [`#3405`](https://github.com/gradido/gradido/pull/3405)
|
||||
- feat(database): use the same mariadb version everywhere [`#3396`](https://github.com/gradido/gradido/pull/3396)
|
||||
- feat(frontend): scale down two img [`#3393`](https://github.com/gradido/gradido/pull/3393)
|
||||
- feat(other): remove docker autostart [`#3395`](https://github.com/gradido/gradido/pull/3395)
|
||||
- feat(frontend): add feedback fixes + map feature fixes [`#3400`](https://github.com/gradido/gradido/pull/3400)
|
||||
- feat(other): increase e2e page load timeout [`#3394`](https://github.com/gradido/gradido/pull/3394)
|
||||
- chore(release): v2.4.1 beta [`#3388`](https://github.com/gradido/gradido/pull/3388)
|
||||
- chore(release): v2.4.0 beta [`#3387`](https://github.com/gradido/gradido/pull/3387)
|
||||
- feat(backend): auto register new user in humhub [`#3386`](https://github.com/gradido/gradido/pull/3386)
|
||||
- feat(backend): try and catch user sync [`#3385`](https://github.com/gradido/gradido/pull/3385)
|
||||
- feat(backend): increase initialien count [`#3369`](https://github.com/gradido/gradido/pull/3369)
|
||||
- feat(frontend): monterail vue3 migration [`#3383`](https://github.com/gradido/gradido/pull/3383)
|
||||
- fix(frontend): fix postmigration fix [`#3382`](https://github.com/gradido/gradido/pull/3382)
|
||||
- feat(frontend): update text [`#3373`](https://github.com/gradido/gradido/pull/3373)
|
||||
- feat(frontend): fix postmigration fix [`#3378`](https://github.com/gradido/gradido/pull/3378)
|
||||
- feat(frontend): map feature in vue 3 [`#3376`](https://github.com/gradido/gradido/pull/3376)
|
||||
- feat(frontend): links and emails in messages [`#3377`](https://github.com/gradido/gradido/pull/3377)
|
||||
- feat(frontend): add transaction link in latest transactions [`#3375`](https://github.com/gradido/gradido/pull/3375)
|
||||
- fix(frontend): fix logout issue [`#3374`](https://github.com/gradido/gradido/pull/3374)
|
||||
- fix(frontend): post migration fixes [`#3372`](https://github.com/gradido/gradido/pull/3372)
|
||||
- feat(frontend): vue3 migration [`#3365`](https://github.com/gradido/gradido/pull/3365)
|
||||
- fix(frontend): fix index.html [`#3368`](https://github.com/gradido/gradido/pull/3368)
|
||||
- build(frontend): merged code from master [`#3367`](https://github.com/gradido/gradido/pull/3367)
|
||||
- fix(frontend): vue3 migration pre deploy setup [`#3366`](https://github.com/gradido/gradido/pull/3366)
|
||||
- fix(workflow): fix broken tests [`#3363`](https://github.com/gradido/gradido/pull/3363)
|
||||
- fix(frontend): style fixes, admin fix [`#3364`](https://github.com/gradido/gradido/pull/3364)
|
||||
- fix(frontend): gdt test [`#3361`](https://github.com/gradido/gradido/pull/3361)
|
||||
- fix(frontend): style fixes [`#3360`](https://github.com/gradido/gradido/pull/3360)
|
||||
- fix(frontend): migration feedback fixes [`#3359`](https://github.com/gradido/gradido/pull/3359)
|
||||
- fix(frontend): scss changes and fixes [`#3358`](https://github.com/gradido/gradido/pull/3358)
|
||||
- fix(frontend): migration remaining fixes [`#3356`](https://github.com/gradido/gradido/pull/3356)
|
||||
- fix(admin): fix message update [`#3354`](https://github.com/gradido/gradido/pull/3354)
|
||||
- fix(admin): fix refetch data in edit creation form [`#3353`](https://github.com/gradido/gradido/pull/3353)
|
||||
- fix(frontend): fix dropdown in transaction send and link [`#3352`](https://github.com/gradido/gradido/pull/3352)
|
||||
- fix(frontend): fix newsletter state reactivity [`#3351`](https://github.com/gradido/gradido/pull/3351)
|
||||
- fix(frontend): fix how community switch is handled [`#3350`](https://github.com/gradido/gradido/pull/3350)
|
||||
- fix(frontend): fixed after merge [`#3349`](https://github.com/gradido/gradido/pull/3349)
|
||||
- fix(frontend): fixed logout handler [`#3347`](https://github.com/gradido/gradido/pull/3347)
|
||||
- chore(frontend): main js cleanup [`#3346`](https://github.com/gradido/gradido/pull/3346)
|
||||
- feature(frontend): change env config reading [`#3345`](https://github.com/gradido/gradido/pull/3345)
|
||||
- feature(frontend): bump node in FE .nvmrc [`#3344`](https://github.com/gradido/gradido/pull/3344)
|
||||
- feat(frontend): migration setup [`#3342`](https://github.com/gradido/gradido/pull/3342)
|
||||
- fix(admin): Remove "maxAmountPerMonth" from `createContributionLink` gql. [`#3343`](https://github.com/gradido/gradido/pull/3343)
|
||||
- fix(admin): style fixes [`#3339`](https://github.com/gradido/gradido/pull/3339)
|
||||
- fix(admin): creation tab disappearing after creating creation [`#3338`](https://github.com/gradido/gradido/pull/3338)
|
||||
- fix(frontend): show updated gdd amount [`#3337`](https://github.com/gradido/gradido/pull/3337)
|
||||
- feat(admin): Add remaining fixes [`#3336`](https://github.com/gradido/gradido/pull/3336)
|
||||
- feat(admin): fix edit creation form [`#3334`](https://github.com/gradido/gradido/pull/3334)
|
||||
- feat(admin): migration of admin creation components [`#3333`](https://github.com/gradido/gradido/pull/3333)
|
||||
- feat(admin): automatic contributions updates [`#3332`](https://github.com/gradido/gradido/pull/3332)
|
||||
- feat(admin): vite config changes [`#3331`](https://github.com/gradido/gradido/pull/3331)
|
||||
- feat(admin) - fix import in node server [`#3330`](https://github.com/gradido/gradido/pull/3330)
|
||||
- fix(admin): stylelint fix [`#3329`](https://github.com/gradido/gradido/pull/3329)
|
||||
- feat(admin): setup migration environment [`#3328`](https://github.com/gradido/gradido/pull/3328)
|
||||
- fix(admin): fix contribution link [`#3326`](https://github.com/gradido/gradido/pull/3326)
|
||||
- feat(admin): geo-coordinates for community [`#3323`](https://github.com/gradido/gradido/pull/3323)
|
||||
- feat(backend): speedup listTransactions [`#3324`](https://github.com/gradido/gradido/pull/3324)
|
||||
- fix(frontend): link forwarding after using send with url parameters [`#3322`](https://github.com/gradido/gradido/pull/3322)
|
||||
|
||||
#### [2.3.1](https://github.com/gradido/gradido/compare/2.3.0...2.3.1)
|
||||
|
||||
> 11 June 2024
|
||||
|
||||
- chore(release): v2.3.1 beta [`#3321`](https://github.com/gradido/gradido/pull/3321)
|
||||
- feat(frontend): more compatible humhub auto-login link [`#3319`](https://github.com/gradido/gradido/pull/3319)
|
||||
- fix(backend): fix test which will only fail at 31. of month, or 30.05 [`#3320`](https://github.com/gradido/gradido/pull/3320)
|
||||
- feat(frontend): remove automatically logged out message [`#3318`](https://github.com/gradido/gradido/pull/3318)
|
||||
|
||||
1
admin/.gitignore
vendored
1
admin/.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
node_modules/
|
||||
build/
|
||||
.cache/
|
||||
.yarn/install-state.gz
|
||||
|
||||
/.env
|
||||
/.env.bak
|
||||
|
||||
@ -11,7 +11,7 @@ ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
|
||||
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
|
||||
ENV BUILD_VERSION="0.0.0.0"
|
||||
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
|
||||
ENV BUILD_COMMIT="0000000"
|
||||
ENV BUILD_COMMIT_SHORT="0000000"
|
||||
## SET NODE_ENV
|
||||
ARG NODE_ENV="production"
|
||||
## App relevant Envs
|
||||
@ -42,6 +42,8 @@ EXPOSE ${PORT}
|
||||
RUN mkdir -p ${DOCKER_WORKDIR}
|
||||
WORKDIR ${DOCKER_WORKDIR}
|
||||
|
||||
RUN mkdir -p /config
|
||||
|
||||
##################################################################################
|
||||
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
|
||||
##################################################################################
|
||||
@ -53,7 +55,7 @@ FROM base as development
|
||||
# Run command
|
||||
# (for development we need to execute yarn install since the
|
||||
# node_modules are on another volume and need updating)
|
||||
CMD /bin/sh -c "yarn install && yarn run dev"
|
||||
CMD /bin/sh -c "cd /config && yarn install && cd /app && yarn && yarn run dev"
|
||||
|
||||
##################################################################################
|
||||
# BUILD (Does contain all files and is therefore bloated) ########################
|
||||
@ -61,9 +63,16 @@ CMD /bin/sh -c "yarn install && yarn run dev"
|
||||
FROM base as build
|
||||
|
||||
# Copy everything
|
||||
COPY . .
|
||||
# yarn install
|
||||
COPY ./admin/ .
|
||||
# Copy everything from config
|
||||
COPY ./config/ ../config/
|
||||
|
||||
# yarn install and build config
|
||||
RUN cd ../config && yarn install --production=false --frozen-lockfile --non-interactive && yarn build
|
||||
|
||||
# yarn install admin
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
|
||||
# yarn build
|
||||
RUN yarn run build
|
||||
|
||||
@ -85,6 +94,7 @@ FROM base as production
|
||||
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../config/build ../config/build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
# Copy static files
|
||||
|
||||
@ -3,9 +3,8 @@
|
||||
"description": "Administration Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "2.3.1",
|
||||
"version": "2.4.5",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"start": "node run/server.js",
|
||||
"dev": "vite",
|
||||
@ -37,7 +36,7 @@
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-vue": "^2.0.2",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-vue-next": "^0.23.2",
|
||||
"bootstrap-vue-next": "0.26.8",
|
||||
"date-fns": "^2.29.3",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"express": "^4.17.1",
|
||||
@ -50,10 +49,11 @@
|
||||
"sass": "^1.77.8",
|
||||
"vite": "3.2.10",
|
||||
"vite-plugin-commonjs": "^0.10.1",
|
||||
"vue": "3.4.31",
|
||||
"vue": "3.5.13",
|
||||
"vue-apollo": "3.1.2",
|
||||
"vue-i18n": "9.13.1",
|
||||
"vue-router": "4.4.0",
|
||||
"vue3-datepicker": "^0.4.0",
|
||||
"vuex": "4.1.0",
|
||||
"vuex-persistedstate": "4.1.0"
|
||||
},
|
||||
@ -74,6 +74,8 @@
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-promise": "^5.1.1",
|
||||
"eslint-plugin-vue": "8.7.1",
|
||||
"gradido-config": "../config",
|
||||
"joi": "^17.13.3",
|
||||
"jsdom": "^25.0.0",
|
||||
"mock-apollo-client": "^1.2.1",
|
||||
"postcss": "^8.4.8",
|
||||
|
||||
7
admin/prepare-and-build.sh
Executable file
7
admin/prepare-and-build.sh
Executable file
@ -0,0 +1,7 @@
|
||||
# TODO this is the quick&dirty solution for the openssl security topic, please see https://stackoverflow.com/questions/69692842/error-message-error0308010cdigital-envelope-routinesunsupported
|
||||
$env:NODE_OPTIONS = "--openssl-legacy-provider"
|
||||
|
||||
nvm use
|
||||
yarn cache clean
|
||||
yarn install
|
||||
yarn build
|
||||
@ -1,11 +1,12 @@
|
||||
// Imports
|
||||
import CONFIG from '../src/config'
|
||||
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
|
||||
// Host & Port
|
||||
const hostname = '127.0.0.1'
|
||||
const port = import.meta.env.PORT || 8080
|
||||
|
||||
const hostname = CONFIG.ADMIN_MODULE_HOST // '127.0.0.1'
|
||||
const port = CONFIG.ADMIN_MODULE_PORT // process.env.PORT || 8080
|
||||
// Express Server
|
||||
const app = express()
|
||||
// Serve files
|
||||
|
||||
@ -125,6 +125,7 @@ import { createContributionLink } from '@/graphql/createContributionLink.js'
|
||||
import { updateContributionLink } from '@/graphql/updateContributionLink.js'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDateFormatter } from '@/composables/useDateFormatter'
|
||||
|
||||
const props = defineProps({
|
||||
contributionLinkData: {
|
||||
@ -138,6 +139,8 @@ const emit = defineEmits(['get-contribution-links', 'close-contribution-form'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { formatDateFromDateTime } = useDateFormatter()
|
||||
|
||||
const contributionLinkForm = ref(null)
|
||||
|
||||
const form = ref({
|
||||
@ -201,11 +204,6 @@ const onSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const formatDateFromDateTime = (datetimeString) => {
|
||||
if (!datetimeString || !datetimeString?.includes('T')) return datetimeString
|
||||
return datetimeString.split('T')[0]
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
form.value = { validFrom: null, validTo: null }
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ vi.mock('@vue/apollo-composable', () => ({
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
locale: { value: 'en' },
|
||||
t: (key) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
@ -8,10 +8,18 @@
|
||||
</BFormCheckbox>
|
||||
</BFormGroup>
|
||||
<BFormGroup v-if="showResubmissionDate">
|
||||
<BFormInput v-model="resubmissionDate" type="date" :min="now"></BFormInput>
|
||||
<time-picker v-model="resubmissionTime"></time-picker>
|
||||
<div class="d-flex my-2">
|
||||
<Datepicker
|
||||
v-model="resubmissionDate"
|
||||
:locale="dateLocale"
|
||||
input-format="P"
|
||||
:lower-limit="now"
|
||||
class="form-control"
|
||||
/>
|
||||
<time-picker v-model="resubmissionTime" class="ms-2" />
|
||||
</div>
|
||||
</BFormGroup>
|
||||
<BTabs v-model="tabindex" content-class="mt-3" data-test="message-type-tabs">
|
||||
<BTabs v-model="tabindex" class="mt-3" content-class="mt-3" data-test="message-type-tabs">
|
||||
<BTab active>
|
||||
<template #title>
|
||||
<span id="message-tab-title">{{ $t('moderator.message') }}</span>
|
||||
@ -24,7 +32,7 @@
|
||||
v-model="form.text"
|
||||
:placeholder="$t('contributionLink.memo')"
|
||||
rows="3"
|
||||
></BFormTextarea>
|
||||
/>
|
||||
</BTab>
|
||||
<BTab>
|
||||
<template #title>
|
||||
@ -38,7 +46,7 @@
|
||||
v-model="form.text"
|
||||
:placeholder="$t('moderator.notice')"
|
||||
rows="3"
|
||||
></BFormTextarea>
|
||||
/>
|
||||
</BTab>
|
||||
<BTab>
|
||||
<template #title>
|
||||
@ -52,7 +60,7 @@
|
||||
v-model="form.memo"
|
||||
:placeholder="$t('contributionLink.memo')"
|
||||
rows="3"
|
||||
></BFormTextarea>
|
||||
/>
|
||||
</BTab>
|
||||
</BTabs>
|
||||
<BRow class="mt-4 mb-6">
|
||||
@ -78,9 +86,10 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useDateLocale } from '@/composables/useDateLocale'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Datepicker from 'vue3-datepicker'
|
||||
import TimePicker from '@/components/input/TimePicker'
|
||||
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
|
||||
import { adminUpdateContribution } from '@/graphql/adminUpdateContribution'
|
||||
@ -114,8 +123,8 @@ const emit = defineEmits([
|
||||
])
|
||||
|
||||
const { t } = useI18n()
|
||||
const dateLocale = useDateLocale()
|
||||
const { toastError, toastSuccess } = useAppToast()
|
||||
|
||||
const form = ref({
|
||||
text: '',
|
||||
memo: props.contributionMemo,
|
||||
@ -141,14 +150,20 @@ const messageType = {
|
||||
MODERATOR: 'MODERATOR',
|
||||
}
|
||||
|
||||
const disabled = computed(() => {
|
||||
return (
|
||||
(tabindex.value === 0 && form.value.text === '') ||
|
||||
const isTextTabValid = computed(() => form.value.text !== '')
|
||||
|
||||
const isMemoTabValid = computed(() => form.value.memo.length >= 5)
|
||||
|
||||
const disabled = computed(
|
||||
() =>
|
||||
loading.value ||
|
||||
(tabindex.value === 1 && form.value.memo.length < 5) ||
|
||||
(showResubmissionDate.value && !resubmissionDate.value)
|
||||
)
|
||||
})
|
||||
(!(showResubmissionDate.value && resubmissionDate.value) &&
|
||||
([0, 1].includes(tabindex.value)
|
||||
? !isTextTabValid.value
|
||||
: tabindex.value === 2
|
||||
? !isMemoTabValid.value
|
||||
: false)),
|
||||
)
|
||||
|
||||
const now = computed(() => new Date())
|
||||
|
||||
|
||||
@ -35,7 +35,10 @@
|
||||
@reset="resetHomeCommunityEditable"
|
||||
>
|
||||
<template #view>
|
||||
<label>{{ $t('federation.gmsApiKey') }} {{ gmsApiKey }}</label>
|
||||
<div class="d-flex">
|
||||
<p style="text-wrap: nowrap">{{ $t('federation.gmsApiKey') }} </p>
|
||||
<span class="d-block" style="overflow-x: auto">{{ gmsApiKey }}</span>
|
||||
</div>
|
||||
<BFormGroup>
|
||||
{{ $t('federation.coordinates') }}
|
||||
<span v-if="isValidLocation">
|
||||
@ -86,9 +89,9 @@
|
||||
<script setup>
|
||||
import { ref, computed, toRefs } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDateLocale } from '@/composables/useDateLocale'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
import EditableGroup from '@/components/input/EditableGroup.vue'
|
||||
import FederationVisualizeItem from './FederationVisualizeItem.vue'
|
||||
import { updateHomeCommunity } from '@/graphql/updateHomeCommunity'
|
||||
@ -96,15 +99,13 @@ import Coordinates from '../input/Coordinates.vue'
|
||||
import EditableGroupableLabel from '../input/EditableGroupableLabel.vue'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const { item } = toRefs(props)
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { t } = useI18n()
|
||||
|
||||
const { toastSuccess, toastError } = useAppToast()
|
||||
|
||||
@ -144,7 +145,7 @@ const lastAnnouncedAt = computed(() => {
|
||||
return formatDistanceToNow(lastAnnouncedAt, {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[locale.value],
|
||||
locale: useDateLocale(),
|
||||
})
|
||||
}
|
||||
return ''
|
||||
@ -155,7 +156,7 @@ const createdAt = computed(() => {
|
||||
return formatDistanceToNow(new Date(item.value.createdAt), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[locale.value],
|
||||
locale: useDateLocale(),
|
||||
})
|
||||
}
|
||||
return ''
|
||||
@ -198,131 +199,3 @@ const resetHomeCommunityEditable = () => {
|
||||
gmsApiKey.value = originalGmsApiKey.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--<script>-->
|
||||
<!--import { formatDistanceToNow } from 'date-fns'-->
|
||||
<!--import { de, enUS as en, fr, es, nl } from 'date-fns/locale'-->
|
||||
<!--import EditableGroup from '@/components/input/EditableGroup'-->
|
||||
<!--import FederationVisualizeItem from './FederationVisualizeItem.vue'-->
|
||||
<!--import { updateHomeCommunity } from '../../graphql/updateHomeCommunity'-->
|
||||
<!--import Coordinates from '../input/Coordinates.vue'-->
|
||||
<!--import EditableGroupableLabel from '../input/EditableGroupableLabel.vue'-->
|
||||
|
||||
<!--const locales = { en, de, es, fr, nl }-->
|
||||
|
||||
<!--export default {-->
|
||||
<!-- name: 'CommunityVisualizeItem',-->
|
||||
<!-- components: {-->
|
||||
<!-- Coordinates,-->
|
||||
<!-- EditableGroup,-->
|
||||
<!-- FederationVisualizeItem,-->
|
||||
<!-- EditableGroupableLabel,-->
|
||||
<!-- },-->
|
||||
<!-- props: {-->
|
||||
<!-- item: { type: Object },-->
|
||||
<!-- },-->
|
||||
<!-- data() {-->
|
||||
<!-- return {-->
|
||||
<!-- formatDistanceToNow,-->
|
||||
<!-- locale: this.$i18n.locale,-->
|
||||
<!-- details: false,-->
|
||||
<!-- gmsApiKey: this.item.gmsApiKey,-->
|
||||
<!-- location: this.item.location,-->
|
||||
<!-- originalGmsApiKey: this.item.gmsApiKey,-->
|
||||
<!-- originalLocation: this.item.location,-->
|
||||
<!-- }-->
|
||||
<!-- },-->
|
||||
<!-- computed: {-->
|
||||
<!-- verified() {-->
|
||||
<!-- if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) {-->
|
||||
<!-- return false-->
|
||||
<!-- }-->
|
||||
<!-- return (-->
|
||||
<!-- this.item.federatedCommunities.filter(-->
|
||||
<!-- (federatedCommunity) =>-->
|
||||
<!-- new Date(federatedCommunity.verifiedAt) >= new Date(federatedCommunity.lastAnnouncedAt),-->
|
||||
<!-- ).length > 0-->
|
||||
<!-- )-->
|
||||
<!-- },-->
|
||||
<!-- icon() {-->
|
||||
<!-- return this.verified ? 'check' : 'x-circle'-->
|
||||
<!-- },-->
|
||||
<!-- variant() {-->
|
||||
<!-- return this.verified ? 'success' : 'danger'-->
|
||||
<!-- },-->
|
||||
<!-- lastAnnouncedAt() {-->
|
||||
<!-- if (!this.item.federatedCommunities || this.item.federatedCommunities.length === 0) return ''-->
|
||||
<!-- const minDate = new Date(0)-->
|
||||
<!-- const lastAnnouncedAt = this.item.federatedCommunities.reduce(-->
|
||||
<!-- (lastAnnouncedAt, federateCommunity) => {-->
|
||||
<!-- if (!federateCommunity.lastAnnouncedAt) return lastAnnouncedAt-->
|
||||
<!-- const date = new Date(federateCommunity.lastAnnouncedAt)-->
|
||||
<!-- return date > lastAnnouncedAt ? date : lastAnnouncedAt-->
|
||||
<!-- },-->
|
||||
<!-- minDate,-->
|
||||
<!-- )-->
|
||||
<!-- if (lastAnnouncedAt !== minDate) {-->
|
||||
<!-- return formatDistanceToNow(lastAnnouncedAt, {-->
|
||||
<!-- includeSecond: true,-->
|
||||
<!-- addSuffix: true,-->
|
||||
<!-- locale: locales[this.locale],-->
|
||||
<!-- })-->
|
||||
<!-- }-->
|
||||
<!-- return ''-->
|
||||
<!-- },-->
|
||||
<!-- createdAt() {-->
|
||||
<!-- if (this.item.createdAt) {-->
|
||||
<!-- return formatDistanceToNow(new Date(this.item.createdAt), {-->
|
||||
<!-- includeSecond: true,-->
|
||||
<!-- addSuffix: true,-->
|
||||
<!-- locale: locales[this.locale],-->
|
||||
<!-- })-->
|
||||
<!-- }-->
|
||||
<!-- return ''-->
|
||||
<!-- },-->
|
||||
<!-- isLocationChanged() {-->
|
||||
<!-- return this.originalLocation !== this.location-->
|
||||
<!-- },-->
|
||||
<!-- isGMSApiKeyChanged() {-->
|
||||
<!-- return this.originalGmsApiKey !== this.gmsApiKey-->
|
||||
<!-- },-->
|
||||
<!-- isValidLocation() {-->
|
||||
<!-- return this.location && this.location.latitude && this.location.longitude-->
|
||||
<!-- },-->
|
||||
<!-- },-->
|
||||
<!-- methods: {-->
|
||||
<!-- toggleDetails() {-->
|
||||
<!-- this.details = !this.details-->
|
||||
<!-- },-->
|
||||
<!-- handleUpdateHomeCommunity() {-->
|
||||
<!-- this.$apollo-->
|
||||
<!-- .mutate({-->
|
||||
<!-- mutation: updateHomeCommunity,-->
|
||||
<!-- variables: {-->
|
||||
<!-- uuid: this.item.uuid,-->
|
||||
<!-- gmsApiKey: this.gmsApiKey,-->
|
||||
<!-- location: this.location,-->
|
||||
<!-- },-->
|
||||
<!-- })-->
|
||||
<!-- .then(() => {-->
|
||||
<!-- if (this.isLocationChanged && this.isGMSApiKeyChanged) {-->
|
||||
<!-- this.toastSuccess(this.$t('federation.toast_gmsApiKeyAndLocationUpdated'))-->
|
||||
<!-- } else if (this.isGMSApiKeyChanged) {-->
|
||||
<!-- this.toastSuccess(this.$t('federation.toast_gmsApiKeyUpdated'))-->
|
||||
<!-- } else if (this.isLocationChanged) {-->
|
||||
<!-- this.toastSuccess(this.$t('federation.toast_gmsLocationUpdated'))-->
|
||||
<!-- }-->
|
||||
<!-- this.originalLocation = this.location-->
|
||||
<!-- this.originalGmsApiKey = this.gmsApiKey-->
|
||||
<!-- })-->
|
||||
<!-- .catch((error) => {-->
|
||||
<!-- this.toastError(error.message)-->
|
||||
<!-- })-->
|
||||
<!-- },-->
|
||||
<!-- resetHomeCommunityEditable() {-->
|
||||
<!-- this.location = this.originalLocation-->
|
||||
<!-- this.gmsApiKey = this.originalGmsApiKey-->
|
||||
<!-- },-->
|
||||
<!-- },-->
|
||||
<!--}-->
|
||||
<!--</script>-->
|
||||
|
||||
@ -30,11 +30,9 @@
|
||||
</template>
|
||||
<script>
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
import { useDateLocale } from '@/composables/useDateLocale'
|
||||
import VariantIcon from '@/components/VariantIcon.vue'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
export default {
|
||||
name: 'FederationVisualizeItem',
|
||||
components: { VariantIcon },
|
||||
@ -58,7 +56,7 @@ export default {
|
||||
? formatDistanceToNow(new Date(dateString), {
|
||||
includeSecond: true,
|
||||
addSuffix: true,
|
||||
locale: locales[this.$i18n.locale],
|
||||
locale: useDateLocale,
|
||||
})
|
||||
: ''
|
||||
},
|
||||
|
||||
@ -114,7 +114,7 @@ describe('NavBar', () => {
|
||||
it('changes window location to wallet and dispatches logout', async () => {
|
||||
const dispatchSpy = vi.spyOn(store, 'dispatch')
|
||||
await wrapper.vm.handleWallet()
|
||||
expect(window.location).toBe(CONFIG.WALLET_AUTH_URL.replace('{token}', 'valid-token'))
|
||||
expect(window.location).toBe(CONFIG.WALLET_AUTH_URL + 'valid-token')
|
||||
expect(dispatchSpy).toHaveBeenCalledWith('logout')
|
||||
})
|
||||
})
|
||||
|
||||
@ -80,7 +80,7 @@ const handleLogout = async () => {
|
||||
}
|
||||
|
||||
const handleWallet = () => {
|
||||
window.location = CONFIG.WALLET_AUTH_URL.replace('{token}', store.state.token)
|
||||
window.location = CONFIG.WALLET_AUTH_URL + store.state.token
|
||||
store.dispatch('logout') // logout without redirect
|
||||
}
|
||||
|
||||
|
||||
@ -3,33 +3,43 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import Coordinates from './Coordinates.vue'
|
||||
import { BFormGroup, BFormInput } from 'bootstrap-vue-next'
|
||||
|
||||
const value = {
|
||||
const modelValue = {
|
||||
latitude: 56.78,
|
||||
longitude: 12.34,
|
||||
}
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key, v) => (key === 'geo-coordinates.format' ? `${v.latitude}, ${v.longitude}` : key),
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockEditableGroup = {
|
||||
valueChanged: vi.fn(function () {
|
||||
this.isValueChanged = true
|
||||
}),
|
||||
invalidValues: vi.fn(function () {
|
||||
this.isValueChanged = false
|
||||
}),
|
||||
}
|
||||
|
||||
describe('Coordinates', () => {
|
||||
let wrapper
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
return mount(Coordinates, {
|
||||
props: {
|
||||
value,
|
||||
modelValue,
|
||||
...props,
|
||||
},
|
||||
global: {
|
||||
mocks: {
|
||||
$t: vi.fn((t, v) => {
|
||||
if (t === 'geo-coordinates.format') {
|
||||
return `${v.latitude}, ${v.longitude}`
|
||||
}
|
||||
return t
|
||||
}),
|
||||
},
|
||||
stubs: {
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
},
|
||||
provide: {
|
||||
editableGroup: mockEditableGroup,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -63,19 +73,13 @@ describe('Coordinates', () => {
|
||||
const latitudeInput = wrapper.find('#home-community-latitude')
|
||||
const longitudeInput = wrapper.find('#home-community-longitude')
|
||||
|
||||
await latitudeInput.setValue(34.56)
|
||||
await latitudeInput.setValue('34.56')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[0][0]).toEqual({
|
||||
latitude: 34.56,
|
||||
longitude: 12.34,
|
||||
})
|
||||
expect(wrapper.vm.inputValue.latitude).toBe('34.56')
|
||||
|
||||
await longitudeInput.setValue('78.90')
|
||||
await longitudeInput.setValue('78.9')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[1][0]).toEqual({
|
||||
latitude: 34.56,
|
||||
longitude: '78.90',
|
||||
})
|
||||
expect(wrapper.vm.inputValue.longitude).toBe('78.9')
|
||||
})
|
||||
|
||||
it('splits coordinates correctly when entering in latitudeLongitude input', async () => {
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<BFormGroup
|
||||
:label="$t('geo-coordinates.label')"
|
||||
:invalid-feedback="$t('geo-coordinates.both-or-none')"
|
||||
:label="t('geo-coordinates.label')"
|
||||
:invalid-feedback="t('geo-coordinates.both-or-none')"
|
||||
:state="isValid"
|
||||
>
|
||||
<BFormGroup
|
||||
:label="$t('latitude-longitude-smart')"
|
||||
:label="t('latitude-longitude-smart')"
|
||||
label-for="home-community-latitude-longitude-smart"
|
||||
:description="$t('geo-coordinates.latitude-longitude-smart.describe')"
|
||||
:description="t('geo-coordinates.latitude-longitude-smart.describe')"
|
||||
>
|
||||
<BFormInput
|
||||
id="home-community-latitude-longitude-smart"
|
||||
@ -17,7 +17,7 @@
|
||||
@input="splitCoordinates"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup :label="$t('latitude')" label-for="home-community-latitude">
|
||||
<BFormGroup :label="t('latitude')" label-for="home-community-latitude">
|
||||
<BFormInput
|
||||
id="home-community-latitude"
|
||||
v-model="inputValue.latitude"
|
||||
@ -25,7 +25,7 @@
|
||||
@input="valueUpdated"
|
||||
/>
|
||||
</BFormGroup>
|
||||
<BFormGroup :label="$t('longitude')" label-for="home-community-longitude">
|
||||
<BFormGroup :label="t('longitude')" label-for="home-community-longitude">
|
||||
<BFormInput
|
||||
id="home-community-longitude"
|
||||
v-model="inputValue.longitude"
|
||||
@ -37,81 +37,94 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Coordinates',
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
<script setup>
|
||||
import { ref, computed, watch, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { BFormGroup, BFormInput } from 'bootstrap-vue-next'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
emits: ['input'],
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
locationString: this.getLatitudeLongitudeString(this.value),
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { t } = useI18n()
|
||||
|
||||
const editableGroup = inject('editableGroup')
|
||||
|
||||
const inputValue = ref(sanitizeLocation(props.modelValue))
|
||||
const originalValue = ref(props.modelValue)
|
||||
const locationString = ref(getLatitudeLongitudeString(props.modelValue))
|
||||
|
||||
const isValid = computed(() => {
|
||||
return (
|
||||
(!isNaN(parseFloat(inputValue.value.longitude)) &&
|
||||
!isNaN(parseFloat(inputValue.value.latitude))) ||
|
||||
(inputValue.value.longitude === '' && inputValue.value.latitude === '')
|
||||
)
|
||||
})
|
||||
|
||||
const isChanged = computed(() => {
|
||||
return inputValue.value !== originalValue.value
|
||||
})
|
||||
|
||||
function splitCoordinates() {
|
||||
const parts = locationString.value.split(',').map((part) => part.trim())
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [lat, lon] = parts
|
||||
if (!isNaN(parseFloat(lon)) && !isNaN(parseFloat(lat))) {
|
||||
inputValue.value.longitude = parseFloat(lon)
|
||||
inputValue.value.latitude = parseFloat(lat)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return (
|
||||
(!isNaN(parseFloat(this.inputValue.longitude)) &&
|
||||
!isNaN(parseFloat(this.inputValue.latitude))) ||
|
||||
(this.inputValue.longitude === '' && this.inputValue.latitude === '')
|
||||
)
|
||||
},
|
||||
isChanged() {
|
||||
return this.inputValue !== this.originalValue
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
splitCoordinates(value) {
|
||||
// default format for geo-coordinates: 'latitude, longitude'
|
||||
const parts = this.locationString.split(',').map((part) => part.trim())
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [lat, lon] = parts
|
||||
if (!isNaN(parseFloat(lon) && !isNaN(parseFloat(lat)))) {
|
||||
this.inputValue.longitude = parseFloat(lon)
|
||||
this.inputValue.latitude = parseFloat(lat)
|
||||
}
|
||||
}
|
||||
this.valueUpdated()
|
||||
},
|
||||
sanitizeLocation(location) {
|
||||
if (!location) return { latitude: '', longitude: '' }
|
||||
|
||||
const parseNumber = (value) => {
|
||||
const number = parseFloat(value)
|
||||
return isNaN(number) ? '' : number
|
||||
}
|
||||
|
||||
return {
|
||||
latitude: parseNumber(location.latitude),
|
||||
longitude: parseNumber(location.longitude),
|
||||
}
|
||||
},
|
||||
getLatitudeLongitudeString({ latitude, longitude } = {}) {
|
||||
return latitude && longitude ? this.$t('geo-coordinates.format', { latitude, longitude }) : ''
|
||||
},
|
||||
valueUpdated() {
|
||||
this.locationString = this.getLatitudeLongitudeString(this.inputValue)
|
||||
this.inputValue = this.sanitizeLocation(this.inputValue)
|
||||
|
||||
if (this.isValid && this.isChanged) {
|
||||
if (this.$parent.valueChanged) {
|
||||
this.$parent.valueChanged()
|
||||
}
|
||||
} else {
|
||||
if (this.$parent.invalidValues) {
|
||||
this.$parent.invalidValues()
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('input', this.inputValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
valueUpdated()
|
||||
}
|
||||
|
||||
function sanitizeLocation(location) {
|
||||
if (!location) return { latitude: '', longitude: '' }
|
||||
|
||||
const parseNumber = (value) => {
|
||||
const number = parseFloat(value)
|
||||
return isNaN(number) ? '' : number
|
||||
}
|
||||
|
||||
return {
|
||||
latitude: parseNumber(location.latitude),
|
||||
longitude: parseNumber(location.longitude),
|
||||
}
|
||||
}
|
||||
|
||||
function getLatitudeLongitudeString(locationData) {
|
||||
return locationData?.latitude && locationData?.longitude
|
||||
? t('geo-coordinates.format', {
|
||||
latitude: locationData.latitude,
|
||||
longitude: locationData.longitude,
|
||||
})
|
||||
: ''
|
||||
}
|
||||
|
||||
function valueUpdated() {
|
||||
locationString.value = getLatitudeLongitudeString(inputValue.value)
|
||||
inputValue.value = sanitizeLocation(inputValue.value)
|
||||
|
||||
if (isValid.value && isChanged.value) {
|
||||
editableGroup.valueChanged()
|
||||
} else {
|
||||
editableGroup.invalidValues()
|
||||
}
|
||||
|
||||
emit('update:modelValue', inputValue.value)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newValue) => {
|
||||
inputValue.value = sanitizeLocation(newValue)
|
||||
originalValue.value = newValue
|
||||
locationString.value = getLatitudeLongitudeString(newValue)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<slot v-if="!isEditing" :is-editing="isEditing" name="view"></slot>
|
||||
<slot v-else :is-editing="isEditing" name="edit" @input="valueChanged"></slot>
|
||||
<slot v-if="!isEditing" :is-editing="isEditing" name="view" />
|
||||
<slot v-else :is-editing="isEditing" name="edit" @update:model-value="valueChanged" />
|
||||
<BFormGroup v-if="allowEdit && !isEditing">
|
||||
<BButton :variant="variant" @click="enableEdit">
|
||||
<IBiPencilFill />
|
||||
@ -12,7 +12,7 @@
|
||||
<BButton :variant="variant" :disabled="!isValueChanged" class="save-button" @click="save">
|
||||
{{ $t('save') }}
|
||||
</BButton>
|
||||
<BButton variant="secondary" class="close-button" @click="close">
|
||||
<BButton variant="secondary" class="close-button ms-2" @click="close">
|
||||
{{ $t('close') }}
|
||||
</BButton>
|
||||
</BFormGroup>
|
||||
@ -22,6 +22,14 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'EditableGroup',
|
||||
provide() {
|
||||
return {
|
||||
editableGroup: {
|
||||
valueChanged: this.valueChanged,
|
||||
invalidValues: this.invalidValues,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
allowEdit: {
|
||||
type: Boolean,
|
||||
@ -58,6 +66,7 @@ export default {
|
||||
close() {
|
||||
this.$emit('reset')
|
||||
this.isEditing = false
|
||||
this.isValueChanged = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import EditableGroupableLabel from './EditableGroupableLabel.vue'
|
||||
import { BFormGroup, BFormInput } from 'bootstrap-vue-next'
|
||||
|
||||
const value = 'test label value'
|
||||
const modelValue = 'test label value'
|
||||
const label = 'Test Label'
|
||||
const idName = 'test-id-name'
|
||||
|
||||
@ -16,7 +16,7 @@ describe('EditableGroupableLabel', () => {
|
||||
components: {
|
||||
EditableGroupableLabel,
|
||||
},
|
||||
props: ['value', 'label', 'idName'],
|
||||
props: ['modelValue', 'label', 'idName'],
|
||||
methods: {
|
||||
onInput: vi.fn(),
|
||||
...parentMethods,
|
||||
@ -24,7 +24,7 @@ describe('EditableGroupableLabel', () => {
|
||||
}
|
||||
return mount(Parent, {
|
||||
props: {
|
||||
value,
|
||||
modelValue,
|
||||
label,
|
||||
idName,
|
||||
...props,
|
||||
@ -55,7 +55,7 @@ describe('EditableGroupableLabel', () => {
|
||||
it('renders BFormInput with correct props', () => {
|
||||
const formInput = wrapper.findComponent({ name: 'BFormInput' })
|
||||
expect(formInput.props('id')).toBe(idName)
|
||||
expect(formInput.props('modelValue')).toBe(value)
|
||||
expect(formInput.props('modelValue')).toBe(modelValue)
|
||||
})
|
||||
|
||||
// it('emits input event with the correct value when input changes', async () => {
|
||||
@ -76,7 +76,7 @@ describe('EditableGroupableLabel', () => {
|
||||
|
||||
const newValue = 'new label value'
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
await input.vm.$emit('input', newValue)
|
||||
await input.vm.$emit('update:model-value', newValue)
|
||||
|
||||
expect(valueChangedMock).toHaveBeenCalled()
|
||||
})
|
||||
@ -86,8 +86,8 @@ describe('EditableGroupableLabel', () => {
|
||||
wrapper = createWrapper({}, { invalidValues: invalidValuesMock })
|
||||
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
await input.vm.$emit('input', 'new label value')
|
||||
await input.vm.$emit('input', value)
|
||||
await input.vm.$emit('update:model-value', 'new label value')
|
||||
await input.vm.$emit('update:model-value', modelValue)
|
||||
|
||||
expect(invalidValuesMock).toHaveBeenCalled()
|
||||
})
|
||||
@ -97,7 +97,7 @@ describe('EditableGroupableLabel', () => {
|
||||
wrapper = createWrapper({}, { valueChanged: valueChangedMock })
|
||||
|
||||
const input = wrapper.findComponent({ name: 'BFormInput' })
|
||||
await input.vm.$emit('input', value)
|
||||
await input.vm.$emit('input', modelValue)
|
||||
|
||||
expect(valueChangedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<BFormGroup :label="label" :label-for="idName">
|
||||
<BFormInput :id="idName" v-model="inputValue" @input="updateValue" />
|
||||
<BFormInput :id="idName" :model-value="modelValue" @update:model-value="inputValue = $event" />
|
||||
</BFormGroup>
|
||||
</template>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
export default {
|
||||
name: 'EditableGroupableLabel',
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
@ -22,16 +22,20 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
emits: ['update:model-value'],
|
||||
data() {
|
||||
return {
|
||||
inputValue: this.value,
|
||||
originalValue: this.value,
|
||||
inputValue: this.modelValue,
|
||||
originalValue: this.modelValue,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inputValue() {
|
||||
this.updateValue()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue(value) {
|
||||
this.inputValue = value
|
||||
updateValue() {
|
||||
if (this.inputValue !== this.originalValue) {
|
||||
if (this.$parent.valueChanged) {
|
||||
this.$parent.valueChanged()
|
||||
@ -41,7 +45,7 @@ export default {
|
||||
this.$parent.invalidValues()
|
||||
}
|
||||
}
|
||||
this.$emit('input', this.inputValue)
|
||||
this.$emit('update:model-value', this.inputValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -16,22 +16,26 @@ describe('TimePicker', () => {
|
||||
await input.setValue('23:45')
|
||||
|
||||
// Check if timeValue is updated
|
||||
expect(wrapper.vm.timeValue).toBe('23:45')
|
||||
expect(wrapper.vm.timeValue).toBe('23:45') // test for Vue 3 composition state directly, if possible
|
||||
|
||||
// Check if update:modelValue event is emitted with updated value
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[0]).toEqual(['23:45'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['23:45'])
|
||||
})
|
||||
|
||||
it('validates and corrects time format on blur', async () => {
|
||||
const wrapper = mount(TimePicker)
|
||||
const wrapper = mount(TimePicker, {
|
||||
props: {
|
||||
modelValue: '99:99', // Set an invalid value initially
|
||||
},
|
||||
})
|
||||
|
||||
const input = wrapper.find('input[type="text"]')
|
||||
|
||||
// Simulate user input
|
||||
await input.setValue('99:99')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[0]).toEqual(['99:99'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['99:99'])
|
||||
|
||||
// Trigger blur event
|
||||
await input.trigger('blur')
|
||||
@ -40,12 +44,16 @@ describe('TimePicker', () => {
|
||||
expect(wrapper.vm.timeValue).toBe('23:59') // Maximum allowed value for hours and minutes
|
||||
|
||||
// Check if update:modelValue event is emitted with corrected value
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[1]).toEqual(['23:59'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')[1]).toEqual(['23:59'])
|
||||
})
|
||||
|
||||
it('checks handling of empty input', async () => {
|
||||
const wrapper = mount(TimePicker)
|
||||
const wrapper = mount(TimePicker, {
|
||||
props: {
|
||||
modelValue: '', // Set initial empty value
|
||||
},
|
||||
})
|
||||
const input = wrapper.find('input[type="text"]')
|
||||
|
||||
// Simulate user input with empty string
|
||||
@ -58,7 +66,7 @@ describe('TimePicker', () => {
|
||||
expect(wrapper.vm.timeValue).toBe('00:00')
|
||||
|
||||
// Check if update:modelValue event is emitted with default value
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')[1]).toEqual(['00:00'])
|
||||
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:modelValue')[1]).toEqual(['00:00'])
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
<div>
|
||||
<input
|
||||
v-model="timeValue"
|
||||
class="timer-input"
|
||||
type="text"
|
||||
placeholder="hh:mm"
|
||||
@input="updateValues"
|
||||
@ -11,39 +12,61 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
export default {
|
||||
// Code written from chatGPT 3.5
|
||||
name: 'TimePicker',
|
||||
props: {
|
||||
value: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: '00:00',
|
||||
},
|
||||
},
|
||||
emits: ['input'],
|
||||
data() {
|
||||
return {
|
||||
timeValue: this.value,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateValues(event) {
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
// reactive state
|
||||
const timeValue = ref(props.modelValue)
|
||||
|
||||
// watch for prop changes
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
timeValue.value = newVal
|
||||
},
|
||||
)
|
||||
|
||||
const updateValues = (event) => {
|
||||
// Allow only numbers and ":"
|
||||
const inputValue = event.target.value.replace(/[^0-9:]/g, '')
|
||||
this.timeValue = inputValue
|
||||
this.$emit('input', inputValue)
|
||||
},
|
||||
validateAndCorrect() {
|
||||
let [hours, minutes] = this.timeValue.split(':')
|
||||
timeValue.value = inputValue
|
||||
emit('update:modelValue', inputValue)
|
||||
}
|
||||
|
||||
const validateAndCorrect = () => {
|
||||
let [hours, minutes] = timeValue.value.split(':')
|
||||
|
||||
// Validate hours and minutes
|
||||
hours = Math.min(parseInt(hours) || 0, 23)
|
||||
minutes = Math.min(parseInt(minutes) || 0, 59)
|
||||
|
||||
// Update the value with correct format
|
||||
this.timeValue = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
|
||||
this.$emit('input', this.timeValue)
|
||||
},
|
||||
timeValue.value = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`
|
||||
emit('update:modelValue', timeValue.value)
|
||||
}
|
||||
|
||||
return {
|
||||
timeValue,
|
||||
updateValues,
|
||||
validateAndCorrect,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.timer-input {
|
||||
border: 1px solid rgb(222 226 230);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
10
admin/src/composables/useDateFormatter.js
Normal file
10
admin/src/composables/useDateFormatter.js
Normal file
@ -0,0 +1,10 @@
|
||||
export const useDateFormatter = () => {
|
||||
const formatDateFromDateTime = (datetimeString) => {
|
||||
if (!datetimeString || !datetimeString?.includes('T')) return datetimeString
|
||||
return datetimeString.split('T')[0]
|
||||
}
|
||||
|
||||
return {
|
||||
formatDateFromDateTime,
|
||||
}
|
||||
}
|
||||
10
admin/src/composables/useDateLocale.js
Normal file
10
admin/src/composables/useDateLocale.js
Normal file
@ -0,0 +1,10 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { de, enUS as en, fr, es, nl } from 'date-fns/locale'
|
||||
|
||||
const locales = { en, de, es, fr, nl }
|
||||
|
||||
export function useDateLocale() {
|
||||
const { locale } = useI18n()
|
||||
const dateLocale = locales[locale.value] || en
|
||||
return dateLocale
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'bootstrap-vue-next'
|
||||
import { useToastController } from 'bootstrap-vue-next'
|
||||
|
||||
export function useAppToast() {
|
||||
const { t } = useI18n()
|
||||
const { show } = useToast()
|
||||
const { show } = useToastController()
|
||||
const toastSuccess = (message) => {
|
||||
toast(message, {
|
||||
title: t('success'),
|
||||
|
||||
@ -4,39 +4,46 @@
|
||||
// Load Package Details for some default values
|
||||
const pkg = require('../../package')
|
||||
|
||||
const constants = {
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v2.2024-01-04',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const version = {
|
||||
ADMIN_MODULE_PROTOCOL: process.env.ADMIN_MODULE_PROTOCOL ?? 'http',
|
||||
ADMIN_MODULE_HOST: process.env.ADMIN_MODULE_HOST ?? '0.0.0.0',
|
||||
ADMIN_MODULE_PORT: process.env.ADMIN_MODULE_PORT ?? '8080',
|
||||
APP_VERSION: pkg.version,
|
||||
BUILD_COMMIT: process.env.BUILD_COMMIT ?? null,
|
||||
BUILD_COMMIT: process.env.BUILD_COMMIT ?? undefined,
|
||||
// self reference of `version.BUILD_COMMIT` is not possible at this point, hence the duplicate code
|
||||
BUILD_COMMIT_SHORT: (process.env.BUILD_COMMIT ?? '0000000').slice(0, 7),
|
||||
PORT: process.env.PORT ?? 8080,
|
||||
}
|
||||
|
||||
let ADMIN_MODULE_URL
|
||||
// in case of hosting the admin module with a nodejs-instance
|
||||
if (process.env.ADMIN_HOSTING === 'nodejs') {
|
||||
ADMIN_MODULE_URL =
|
||||
version.ADMIN_MODULE_PROTOCOL +
|
||||
'://' +
|
||||
version.ADMIN_MODULE_HOST +
|
||||
':' +
|
||||
version.ADMIN_MODULE_PORT
|
||||
} else {
|
||||
// in case of hosting the admin module with a nginx
|
||||
ADMIN_MODULE_URL = version.ADMIN_MODULE_PROTOCOL + '://' + version.ADMIN_MODULE_HOST
|
||||
}
|
||||
|
||||
const environment = {
|
||||
NODE_ENV: import.meta.env.NODE_ENV,
|
||||
DEBUG: import.meta.env.NODE_ENV !== 'production' ?? false,
|
||||
PRODUCTION: import.meta.env.NODE_ENV === 'production' ?? false,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
DEBUG: process.env.NODE_ENV !== 'production' ?? false,
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' ?? false,
|
||||
}
|
||||
|
||||
const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? undefined
|
||||
const URL_PROTOCOL = process.env.URL_PROTOCOL ?? 'http'
|
||||
const COMMUNITY_URL =
|
||||
COMMUNITY_HOST && URL_PROTOCOL ? URL_PROTOCOL + '://' + COMMUNITY_HOST : undefined
|
||||
const WALLET_URL = process.env.WALLET_URL ?? COMMUNITY_URL ?? 'http://localhost'
|
||||
// const COMMUNITY_HOST = process.env.COMMUNITY_HOST ?? undefined
|
||||
// const URL_PROTOCOL = process.env.URL_PROTOCOL ?? 'http'
|
||||
// const COMMUNITY_URL =
|
||||
// COMMUNITY_HOST && URL_PROTOCOL ? URL_PROTOCOL + '://' + COMMUNITY_HOST : undefined
|
||||
const COMMUNITY_URL = process.env.COMMUNITY_URL ?? ADMIN_MODULE_URL
|
||||
const WALLET_URL = process.env.WALLET_URL ?? COMMUNITY_URL ?? 'http://0.0.0.0'
|
||||
|
||||
const endpoints = {
|
||||
GRAPHQL_URL:
|
||||
(process.env.GRAPHQL_URL ?? COMMUNITY_URL ?? 'http://localhost:4000') +
|
||||
process.env.GRAPHQL_PATH ?? '/graphql',
|
||||
WALLET_AUTH_URL: WALLET_URL + (process.env.WALLET_AUTH_PATH ?? '/authenticate?token={token}'),
|
||||
GRAPHQL_URI: process.env.GRAPHQL_URL ?? COMMUNITY_URL + (process.env.GRAPHQL_PATH ?? '/graphql'),
|
||||
WALLET_AUTH_URL: WALLET_URL + (process.env.WALLET_AUTH_PATH ?? '/authenticate?token='),
|
||||
WALLET_LOGIN_URL: WALLET_URL + (process.env.WALLET_LOGIN_PATH ?? '/login'),
|
||||
}
|
||||
|
||||
@ -44,24 +51,13 @@ const debug = {
|
||||
DEBUG_DISABLE_AUTH: process.env.DEBUG_DISABLE_AUTH === 'true' ?? false,
|
||||
}
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||
constants.CONFIG_VERSION.CURRENT,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
|
||||
)
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
...constants,
|
||||
...version,
|
||||
...environment,
|
||||
...endpoints,
|
||||
...debug,
|
||||
ADMIN_MODULE_URL,
|
||||
COMMUNITY_URL,
|
||||
}
|
||||
|
||||
export default CONFIG
|
||||
module.exports = CONFIG
|
||||
|
||||
106
admin/src/config/schema.js
Normal file
106
admin/src/config/schema.js
Normal file
@ -0,0 +1,106 @@
|
||||
const {
|
||||
APP_VERSION,
|
||||
BUILD_COMMIT,
|
||||
BUILD_COMMIT_SHORT,
|
||||
COMMUNITY_URL,
|
||||
DEBUG,
|
||||
GRAPHQL_URI,
|
||||
NODE_ENV,
|
||||
PRODUCTION,
|
||||
} = require('gradido-config/build/src/commonSchema.js')
|
||||
const Joi = require('joi')
|
||||
|
||||
module.exports = Joi.object({
|
||||
APP_VERSION,
|
||||
BUILD_COMMIT,
|
||||
BUILD_COMMIT_SHORT,
|
||||
COMMUNITY_URL,
|
||||
DEBUG,
|
||||
GRAPHQL_URI,
|
||||
NODE_ENV,
|
||||
PRODUCTION,
|
||||
|
||||
ADMIN_HOSTING: Joi.string()
|
||||
.valid('nodejs', 'nginx')
|
||||
.description('set to `nodejs` if admin is hosted by vite with a own nodejs instance')
|
||||
.optional(),
|
||||
|
||||
ADMIN_MODULE_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.when('COMMUNITY_URL', {
|
||||
is: Joi.exist(),
|
||||
then: Joi.optional(), // not required if COMMUNITY_URL is provided
|
||||
otherwise: Joi.required(), // required if COMMUNITY_URL is missing
|
||||
})
|
||||
.description("Base Url for reaching admin in browser, only needed if COMMUNITY_URL wasn't set")
|
||||
.optional(), // optional in general, but conditionally required
|
||||
|
||||
ADMIN_MODULE_PROTOCOL: Joi.string()
|
||||
.when('ADMIN_HOSTING', {
|
||||
is: Joi.valid('nodejs'),
|
||||
then: Joi.valid('http').required(),
|
||||
otherwise: Joi.valid('http', 'https').required(),
|
||||
})
|
||||
.description(
|
||||
`
|
||||
Protocol for admin module hosting
|
||||
- it has to be the same as for backend api url and frontend to prevent mixed block errors,
|
||||
- if admin is served with nodejs:
|
||||
is have to be http or setup must be updated to include a ssl certificate
|
||||
`,
|
||||
)
|
||||
.default('http')
|
||||
.required(),
|
||||
|
||||
ADMIN_MODULE_HOST: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string().valid('localhost').messages({ 'any.invalid': 'Must be localhost' }),
|
||||
Joi.string()
|
||||
.ip({ version: ['ipv4'] })
|
||||
.messages({ 'string.ip': 'Must be a valid IPv4 address' }),
|
||||
Joi.string().domain().messages({ 'string.domain': 'Must be a valid domain' }),
|
||||
)
|
||||
.when('ADMIN_HOSTING', {
|
||||
is: 'nodejs',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
})
|
||||
.when('COMMUNITY_URL', {
|
||||
is: null,
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
})
|
||||
.description(
|
||||
'Host (domain, IPv4, or localhost) for the admin, default is 0.0.0.0 for local hosting during develop',
|
||||
)
|
||||
.default('0.0.0.0'),
|
||||
|
||||
ADMIN_MODULE_PORT: Joi.number()
|
||||
.integer()
|
||||
.min(1024)
|
||||
.max(49151)
|
||||
.description('Port for hosting Admin with Vite as a Node.js instance, default: 8080')
|
||||
.default(8080)
|
||||
.when('ADMIN_HOSTING', {
|
||||
is: 'nodejs',
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.optional(),
|
||||
}),
|
||||
|
||||
WALLET_AUTH_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.description('Extern Url from wallet-frontend for forwarding from admin')
|
||||
.default('http://0.0.0.0/authenticate?token=')
|
||||
.required(),
|
||||
|
||||
WALLET_LOGIN_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.description('Extern Url from wallet-frontend for forwarding after logout')
|
||||
.default('http://0.0.0.0/login')
|
||||
.required(),
|
||||
|
||||
DEBUG_DISABLE_AUTH: Joi.boolean()
|
||||
.description('Flag for disable authorization during development')
|
||||
.default(false)
|
||||
.optional(), // true is only allowed in not-production setup
|
||||
})
|
||||
@ -6,57 +6,85 @@ import Components from 'unplugin-vue-components/vite'
|
||||
import IconsResolve from 'unplugin-icons/resolver'
|
||||
import { BootstrapVueNextResolver } from 'bootstrap-vue-next'
|
||||
import EnvironmentPlugin from 'vite-plugin-environment'
|
||||
import schema from './src/config/schema'
|
||||
import { validate, browserUrls } from 'gradido-config/build/src/index.js'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config() // load env vars from .env
|
||||
|
||||
const CONFIG = require('./src/config')
|
||||
|
||||
const path = require('path')
|
||||
|
||||
export default defineConfig({
|
||||
base: '/admin/',
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
assets: path.join(__dirname, 'src/assets'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
export default defineConfig(({ command }) => {
|
||||
if (command === 'serve') {
|
||||
CONFIG.ADMIN_HOSTING = 'nodejs'
|
||||
} else {
|
||||
CONFIG.ADMIN_HOSTING = 'nginx'
|
||||
}
|
||||
// Check config
|
||||
validate(schema, CONFIG)
|
||||
// make sure that all urls used in browser have the same protocol to prevent mixed content errors
|
||||
validate(browserUrls, [
|
||||
CONFIG.ADMIN_AUTH_URL,
|
||||
CONFIG.COMMUNITY_URL,
|
||||
CONFIG.COMMUNITY_REGISTER_URL,
|
||||
CONFIG.GRAPHQL_URI,
|
||||
CONFIG.FRONTEND_MODULE_URL,
|
||||
])
|
||||
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
compatConfig: {
|
||||
MODE: 2,
|
||||
return {
|
||||
base: '/admin/',
|
||||
server: {
|
||||
host: CONFIG.ADMIN_MODULE_HOST, // '0.0.0.0',
|
||||
port: CONFIG.ADMIN_MODULE_PORT, // 8080,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
assets: path.join(__dirname, 'src/assets'),
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
compatConfig: {
|
||||
MODE: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
Components({
|
||||
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
|
||||
dts: true,
|
||||
}),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
}),
|
||||
EnvironmentPlugin({
|
||||
BUILD_COMMIT: null,
|
||||
PORT: null,
|
||||
COMMUNITY_HOST: null,
|
||||
URL_PROTOCOL: null,
|
||||
WALLET_URL: null,
|
||||
GRAPHQL_URL: null,
|
||||
GRAPHQL_PATH: null,
|
||||
WALLET_AUTH_PATH: null,
|
||||
WALLET_LOGIN_PATH: null,
|
||||
DEBUG_DISABLE_AUTH: null,
|
||||
CONFIG_VERSION: null,
|
||||
}),
|
||||
commonjs(),
|
||||
],
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, './build'),
|
||||
},
|
||||
publicDir: '/admin',
|
||||
}),
|
||||
Components({
|
||||
resolvers: [IconsResolve(), BootstrapVueNextResolver()],
|
||||
dts: true,
|
||||
}),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
}),
|
||||
EnvironmentPlugin({
|
||||
BUILD_COMMIT: null,
|
||||
PORT: CONFIG.ADMIN_MODULE_PORT ?? null, // null,
|
||||
COMMUNITY_HOST: CONFIG.ADMIN_MODULE_HOST ?? null, // null,
|
||||
COMMUNITY_URL: CONFIG.COMMUNITY_URL ?? null,
|
||||
URL_PROTOCOL: CONFIG.ADMIN_MODULE_PROTOCOL ?? null, // null,
|
||||
WALLET_AUTH_URL: CONFIG.WALLET_AUTH_URL ?? null,
|
||||
GRAPHQL_URL: CONFIG.GRAPHQL_URI ?? null, // null,
|
||||
GRAPHQL_PATH: process.env.GRAPHQL_PATH ?? '/graphql', // null,
|
||||
WALLET_AUTH_PATH: CONFIG.WALLET_AUTH_PATH ?? null,
|
||||
WALLET_LOGIN_PATH: CONFIG.WALLET_LOGIN_URL ?? null, // null,
|
||||
DEBUG_DISABLE_AUTH: CONFIG.DEBUG_DISABLE_AUTH ?? null, // null,
|
||||
// CONFIG_VERSION: CONFIG.CONFIG_VERSION, // null,
|
||||
}),
|
||||
commonjs(),
|
||||
],
|
||||
build: {
|
||||
outDir: path.resolve(__dirname, './build'),
|
||||
chunkSizeWarningLimit: 1600,
|
||||
},
|
||||
publicDir: '/admin',
|
||||
}
|
||||
})
|
||||
|
||||
@ -7,7 +7,7 @@ const CONFIG = require('./src/config')
|
||||
// vue.config.js
|
||||
module.exports = {
|
||||
devServer: {
|
||||
port: CONFIG.PORT,
|
||||
port: CONFIG.ADMIN_MODULE_PORT,
|
||||
},
|
||||
pluginOptions: {
|
||||
i18n: {
|
||||
|
||||
4065
admin/yarn.lock
4065
admin/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@ PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
JWT_EXPIRES_IN=10m
|
||||
GRAPHIQL=false
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
GDT_ACTIVE=false
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
@ -45,7 +45,7 @@ EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
EMAIL_SMTP_URL=gmail.com
|
||||
EMAIL_SMTP_HOST=gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION_PATH=/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD_PATH=/reset-password/{optin}
|
||||
|
||||
78
backend/.env.org
Normal file
78
backend/.env.org
Normal file
@ -0,0 +1,78 @@
|
||||
# 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
|
||||
DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.backend.log
|
||||
|
||||
# Klicktipp
|
||||
KLICKTIPP=false
|
||||
KLICKTTIPP_API_URL=https://api.klicktipp.com
|
||||
KLICKTIPP_USER=gradido_test
|
||||
KLICKTIPP_PASSWORD=secret321
|
||||
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
# DltConnector
|
||||
DLT_CONNECTOR=true
|
||||
DLT_CONNECTOR_URL=http://localhost:6010
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost
|
||||
COMMUNITY_REGISTER_PATH=/register
|
||||
COMMUNITY_REDEEM_PATH=/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_PATH=/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_TEST_MODUS=false
|
||||
EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
EMAIL_SMTP_HOST=gmail.com
|
||||
EMAIL_SMTP_PORT=587
|
||||
EMAIL_LINK_VERIFICATION_PATH=/checkEmail/{optin}{code}
|
||||
EMAIL_LINK_SETPASSWORD_PATH=/reset-password/{optin}
|
||||
EMAIL_LINK_FORGOTPASSWORD_PATH=/forgot-password
|
||||
EMAIL_LINK_OVERVIEW_PATH=/overview
|
||||
EMAIL_CODE_VALID_TIME=1440
|
||||
EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Federation
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
|
||||
FEDERATION_XCOM_SENDCOINS_ENABLED=false
|
||||
|
||||
# GMS
|
||||
# GMS_ACTIVE=true
|
||||
# Coordinates of Illuminz test instance
|
||||
#GMS_API_URL=http://54.176.169.179:3071
|
||||
GMS_API_URL=http://localhost:4044
|
||||
GMS_DASHBOARD_URL=http://localhost:8080
|
||||
|
||||
# HUMHUB
|
||||
HUMHUB_ACTIVE=true
|
||||
HUMHUB_API_URL=https://community-test.gradido.net
|
||||
HUMHUB_JWT_KEY=GwdkIKi-rkRS0mXC4Cg3MYc3ktZh89VFmntDpNKET_dUfcIdjL_957F3nCv3brNtDfbbV81NViKaktUsfExrkH
|
||||
@ -40,6 +40,7 @@ COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
USE_CRYPTO_WORKER=$USE_CRYPTO_WORKER
|
||||
|
||||
# EMail
|
||||
EMAIL=$EMAIL
|
||||
@ -48,7 +49,7 @@ EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER
|
||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||
EMAIL_SENDER=$EMAIL_SENDER
|
||||
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
||||
EMAIL_SMTP_URL=$EMAIL_SMTP_URL
|
||||
EMAIL_SMTP_HOST=$EMAIL_SMTP_HOST
|
||||
EMAIL_SMTP_PORT=$EMAIL_SMTP_PORT
|
||||
EMAIL_LINK_VERIFICATION_PATH=$EMAIL_LINK_VERIFICATION_PATH
|
||||
EMAIL_LINK_SETPASSWORD_PATH=$EMAIL_LINK_SETPASSWORD_PATH
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
# Server
|
||||
JWT_EXPIRES_IN=2m
|
||||
|
||||
GDT_ACTIVE=false
|
||||
|
||||
# Email
|
||||
EMAIL=true
|
||||
EMAIL_TEST_MODUS=false
|
||||
|
||||
@ -43,6 +43,7 @@ RUN mkdir -p ${DOCKER_WORKDIR}
|
||||
WORKDIR ${DOCKER_WORKDIR}
|
||||
|
||||
RUN mkdir -p /database
|
||||
RUN mkdir -p /config
|
||||
|
||||
##################################################################################
|
||||
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
|
||||
@ -55,7 +56,7 @@ FROM base as development
|
||||
# Run command
|
||||
# (for development we need to execute yarn install since the
|
||||
# node_modules are on another volume and need updating)
|
||||
CMD /bin/sh -c "cd /database && yarn install && yarn build && cd /app && yarn install && yarn run dev"
|
||||
CMD /bin/sh -c "cd /database && yarn install && yarn build && cd /config && yarn install && cd /app && yarn install && yarn run dev"
|
||||
|
||||
##################################################################################
|
||||
# BUILD (Does contain all files and is therefore bloated) ########################
|
||||
@ -66,6 +67,11 @@ FROM base as build
|
||||
COPY ./backend/ ./
|
||||
# Copy everything from database
|
||||
COPY ./database/ ../database/
|
||||
# Copy everything from config
|
||||
COPY ./config/ ../config/
|
||||
|
||||
# yarn install and build config
|
||||
RUN cd ../config && yarn install --production=false --frozen-lockfile --non-interactive && yarn build
|
||||
|
||||
# yarn install backend
|
||||
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
@ -74,10 +80,10 @@ RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||
RUN cd ../database && yarn install --production=false --frozen-lockfile --non-interactive
|
||||
|
||||
# yarn build
|
||||
RUN yarn run build
|
||||
RUN yarn build
|
||||
|
||||
# yarn build database
|
||||
RUN cd ../database && yarn run build
|
||||
RUN cd ../database && yarn build
|
||||
|
||||
##################################################################################
|
||||
# TEST ###########################################################################
|
||||
@ -95,9 +101,11 @@ FROM base as production
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../config/build ../config/build
|
||||
# We also copy the node_modules express and serve-static for the run script
|
||||
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../config/node_modules ../config/node_modules
|
||||
|
||||
# Copy static files
|
||||
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||
|
||||
@ -7,7 +7,7 @@ module.exports = {
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 81,
|
||||
lines: 80,
|
||||
},
|
||||
},
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
@ -39,5 +39,10 @@ module.exports = {
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../database/src/$1'
|
||||
: '<rootDir>/../database/build/src/$1',
|
||||
'@config/(.*)':
|
||||
// eslint-disable-next-line n/no-process-env
|
||||
process.env.NODE_ENV === 'development'
|
||||
? '<rootDir>/../config/src/$1'
|
||||
: '<rootDir>/../config/build/$1',
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "2.3.1",
|
||||
"version": "2.4.5",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -33,12 +33,14 @@
|
||||
"email-templates": "^10.0.1",
|
||||
"express": "^4.17.1",
|
||||
"express-slow-down": "^2.0.1",
|
||||
"gradido-config": "file:../config",
|
||||
"gradido-database": "file:../database",
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-request": "5.0.0",
|
||||
"graphql-type-json": "0.3.2",
|
||||
"helmet": "^5.1.1",
|
||||
"i18n": "^0.15.1",
|
||||
"joi": "^17.13.3",
|
||||
"jose": "^4.14.4",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"log4js": "^6.4.6",
|
||||
@ -50,7 +52,9 @@
|
||||
"sodium-native": "^3.3.0",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typed-rest-client": "^1.8.11",
|
||||
"uuid": "^8.3.2"
|
||||
"uuid": "^8.3.2",
|
||||
"workerpool": "^9.2.0",
|
||||
"xregexp": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-community/eslint-plugin-eslint-comments": "^3.2.1",
|
||||
@ -59,8 +63,9 @@
|
||||
"@types/faker": "^5.5.9",
|
||||
"@types/i18n": "^0.13.4",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@types/joi": "^17.2.3",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@types/node": "^16.10.3",
|
||||
"@types/node": "^17.0.21",
|
||||
"@types/nodemailer": "^6.4.4",
|
||||
"@types/sodium-native": "^2.3.5",
|
||||
"@types/uuid": "^8.3.4",
|
||||
|
||||
5
backend/src/apis/ConnectionAgents.ts
Normal file
5
backend/src/apis/ConnectionAgents.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Agent } from 'http'
|
||||
import { Agent as HttpsAgent } from 'https'
|
||||
|
||||
export const httpAgent = new Agent({ keepAlive: true })
|
||||
export const httpsAgent = new HttpsAgent({ keepAlive: true })
|
||||
@ -7,10 +7,12 @@ import axios from 'axios'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { httpAgent, httpsAgent } from './ConnectionAgents'
|
||||
|
||||
export const apiPost = async (url: string, payload: unknown): Promise<any> => {
|
||||
logger.trace('POST', url, payload)
|
||||
try {
|
||||
const result = await axios.post(url, payload)
|
||||
const result = await axios.post(url, payload, { httpAgent, httpsAgent })
|
||||
logger.trace('POST-Response', result)
|
||||
if (result.status !== 200) {
|
||||
throw new LogError('HTTP Status Error', result.status)
|
||||
@ -27,7 +29,7 @@ export const apiPost = async (url: string, payload: unknown): Promise<any> => {
|
||||
export const apiGet = async (url: string): Promise<any> => {
|
||||
logger.trace('GET: url=' + url)
|
||||
try {
|
||||
const result = await axios.get(url)
|
||||
const result = await axios.get(url, { httpAgent, httpsAgent })
|
||||
logger.trace('GET-Response', result)
|
||||
if (result.status !== 200) {
|
||||
throw new LogError('HTTP Status Error', result.status)
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import axios from 'axios'
|
||||
|
||||
import { httpAgent, httpsAgent } from '@/apis/ConnectionAgents'
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
@ -126,9 +127,10 @@ export async function createGmsUser(apiKey: string, user: GmsUser): Promise<bool
|
||||
accept: 'application/json',
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
connection: 'keep-alive',
|
||||
authorization: apiKey,
|
||||
},
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
}
|
||||
try {
|
||||
const result = await axios.post(baseUrl.concat(service), user, config)
|
||||
@ -160,9 +162,10 @@ export async function updateGmsUser(apiKey: string, user: GmsUser): Promise<bool
|
||||
accept: 'application/json',
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
connection: 'keep-alive',
|
||||
authorization: apiKey,
|
||||
},
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
}
|
||||
try {
|
||||
const result = await axios.patch(baseUrl.concat(service), user, config)
|
||||
@ -197,9 +200,10 @@ export async function verifyAuthToken(
|
||||
accept: 'application/json',
|
||||
language: 'en',
|
||||
timezone: 'UTC',
|
||||
connection: 'keep-alive',
|
||||
// authorization: apiKey,
|
||||
},
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
}
|
||||
try {
|
||||
const result = await axios.get(baseUrl.concat(service), config)
|
||||
|
||||
@ -28,8 +28,8 @@ export class GmsUser {
|
||||
status: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
firstName: string | undefined
|
||||
lastName: string | undefined
|
||||
firstName: string | null | undefined
|
||||
lastName: string | null | undefined
|
||||
alias: string | undefined
|
||||
type: number
|
||||
address: string | undefined
|
||||
@ -48,9 +48,19 @@ export class GmsUser {
|
||||
) {
|
||||
return user.alias
|
||||
}
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
|
||||
) {
|
||||
return (
|
||||
this.firstUpperCaseSecondLowerCase(user.firstName) +
|
||||
this.firstUpperCaseSecondLowerCase(user.lastName)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private getGmsFirstName(user: dbUser): string | undefined {
|
||||
private getGmsFirstName(user: dbUser): string | null | undefined {
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
(user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST ||
|
||||
@ -64,22 +74,30 @@ export class GmsUser {
|
||||
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
|
||||
) {
|
||||
return user.firstName.substring(0, 1)
|
||||
// return this.firstUpperCaseSecondLowerCase(user.firstName)
|
||||
return null // cause to delete firstname in gms
|
||||
}
|
||||
}
|
||||
|
||||
private getGmsLastName(user: dbUser): string | undefined {
|
||||
private getGmsLastName(user: dbUser): string | null | undefined {
|
||||
if (user.gmsAllowed && user.gmsPublishName === PublishNameType.PUBLISH_NAME_FULL) {
|
||||
return user.lastName
|
||||
}
|
||||
if (user.gmsAllowed && user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL) {
|
||||
return this.firstUpperCaseSecondLowerCase(user.lastName)
|
||||
}
|
||||
return null // cause to delete lastname in gms
|
||||
|
||||
/*
|
||||
if (
|
||||
user.gmsAllowed &&
|
||||
((!user.alias && user.gmsPublishName === PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS) ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_FIRST_INITIAL ||
|
||||
user.gmsPublishName === PublishNameType.PUBLISH_NAME_INITIALS)
|
||||
) {
|
||||
return user.lastName.substring(0, 1)
|
||||
return this.firstUpperCaseSecondLowerCase(user.lastName)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private getGmsEmail(user: dbUser): string | undefined {
|
||||
@ -106,4 +124,11 @@ export class GmsUser {
|
||||
return user.emailContact.phone
|
||||
}
|
||||
}
|
||||
|
||||
private firstUpperCaseSecondLowerCase(name: string) {
|
||||
if (name && name.length >= 2) {
|
||||
return name.charAt(0).toUpperCase() + name.charAt(1).toLocaleLowerCase()
|
||||
}
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,9 +9,11 @@ import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||
|
||||
import { HumHubClient } from './HumHubClient'
|
||||
import { GetUser } from './model/GetUser'
|
||||
import { UsersResponse } from './model/UsersResponse'
|
||||
import { ExecutedHumhubAction, syncUser } from './syncUser'
|
||||
|
||||
const USER_BULK_SIZE = 20
|
||||
const HUMHUB_BULK_SIZE = 50
|
||||
|
||||
function getUsersPage(page: number, limit: number): Promise<[User[], number]> {
|
||||
return User.findAndCount({
|
||||
@ -29,24 +31,28 @@ function getUsersPage(page: number, limit: number): Promise<[User[], number]> {
|
||||
async function loadUsersFromHumHub(client: HumHubClient): Promise<Map<string, GetUser>> {
|
||||
const start = new Date().getTime()
|
||||
const humhubUsers = new Map<string, GetUser>()
|
||||
const firstPage = await client.users(0, 50)
|
||||
if (!firstPage) {
|
||||
throw new LogError('not a single user found on humhub, please check config and setup')
|
||||
}
|
||||
firstPage.results.forEach((user) => {
|
||||
humhubUsers.set(user.account.email.trim(), user)
|
||||
})
|
||||
let page = 1
|
||||
while (humhubUsers.size < firstPage.total) {
|
||||
const usersPage = await client.users(page, 50)
|
||||
let page = 0
|
||||
let skippedUsersCount = 0
|
||||
let usersPage: UsersResponse | null = null
|
||||
do {
|
||||
usersPage = await client.users(page, HUMHUB_BULK_SIZE)
|
||||
if (!usersPage) {
|
||||
throw new LogError('error requesting next users page from humhub')
|
||||
}
|
||||
usersPage.results.forEach((user) => {
|
||||
humhubUsers.set(user.account.email.trim(), user)
|
||||
// deleted users have empty emails
|
||||
if (user.account.email) {
|
||||
humhubUsers.set(user.account.email.trim(), user)
|
||||
} else {
|
||||
skippedUsersCount++
|
||||
}
|
||||
})
|
||||
page++
|
||||
}
|
||||
process.stdout.write(
|
||||
`load users from humhub: ${humhubUsers.size}/${usersPage.total}, skipped: ${skippedUsersCount}\r`,
|
||||
)
|
||||
} while (usersPage && usersPage.results.length === HUMHUB_BULK_SIZE)
|
||||
|
||||
const elapsed = new Date().getTime() - start
|
||||
logger.info('load users from humhub', {
|
||||
total: humhubUsers.size,
|
||||
|
||||
@ -20,7 +20,9 @@ export class HumHubClient {
|
||||
|
||||
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
|
||||
private constructor() {
|
||||
this.restClient = new RestClient('gradido-backend', CONFIG.HUMHUB_API_URL)
|
||||
this.restClient = new RestClient('gradido-backend', CONFIG.HUMHUB_API_URL, undefined, {
|
||||
keepAlive: true,
|
||||
})
|
||||
logger.info('create rest client for', CONFIG.HUMHUB_API_URL)
|
||||
}
|
||||
|
||||
|
||||
23
backend/src/apis/humhub/model/AbstractUser.ts
Normal file
23
backend/src/apis/humhub/model/AbstractUser.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
import { Account } from './Account'
|
||||
import { Profile } from './Profile'
|
||||
|
||||
export abstract class AbstractUser {
|
||||
public constructor(user: User) {
|
||||
this.account = new Account(user)
|
||||
this.profile = new Profile(user)
|
||||
// temp fix for prevent double usernames in humhub, if the username ist created from initials
|
||||
const publishNameLogic = new PublishNameLogic(user)
|
||||
if (publishNameLogic.isUsernameFromInitials(user.humhubPublishName as PublishNameType)) {
|
||||
this.profile.firstname = this.account.username
|
||||
this.account.username = user.gradidoID
|
||||
}
|
||||
}
|
||||
|
||||
account: Account
|
||||
profile: Profile
|
||||
}
|
||||
@ -2,15 +2,13 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { convertGradidoLanguageToHumhub } from '@/apis/humhub/convertLanguage'
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
export class Account {
|
||||
public constructor(user: User) {
|
||||
if (user.alias && user.alias.length > 2) {
|
||||
this.username = user.alias
|
||||
} else {
|
||||
this.username = user.gradidoID
|
||||
}
|
||||
|
||||
const publishNameLogic = new PublishNameLogic(user)
|
||||
this.username = publishNameLogic.getUsername(user.humhubPublishName as PublishNameType)
|
||||
this.email = user.emailContact.email
|
||||
this.language = convertGradidoLanguageToHumhub(user.language)
|
||||
this.status = 1
|
||||
|
||||
@ -1,19 +1,15 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { Account } from './Account'
|
||||
import { Profile } from './Profile'
|
||||
import { AbstractUser } from './AbstractUser'
|
||||
|
||||
export class GetUser {
|
||||
export class GetUser extends AbstractUser {
|
||||
public constructor(user: User, id: number) {
|
||||
super(user)
|
||||
this.id = id
|
||||
this.account = new Account(user)
|
||||
this.profile = new Profile(user)
|
||||
}
|
||||
|
||||
id: number
|
||||
guid: string
|
||||
// eslint-disable-next-line camelcase
|
||||
display_name: string
|
||||
account: Account
|
||||
profile: Profile
|
||||
}
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { Account } from './Account'
|
||||
import { AbstractUser } from './AbstractUser'
|
||||
import { Password } from './Password'
|
||||
import { Profile } from './Profile'
|
||||
|
||||
export class PostUser {
|
||||
public constructor(user: User) {
|
||||
this.account = new Account(user)
|
||||
this.profile = new Profile(user)
|
||||
}
|
||||
|
||||
account: Account
|
||||
profile: Profile
|
||||
// only add password as filed, rest the same as AbstractUser
|
||||
export class PostUser extends AbstractUser {
|
||||
password: Password
|
||||
}
|
||||
|
||||
@ -10,7 +10,10 @@ export class Profile {
|
||||
const publishNameLogic = new PublishNameLogic(user)
|
||||
this.firstname = publishNameLogic.getFirstName(user.humhubPublishName as PublishNameType)
|
||||
this.lastname = publishNameLogic.getLastName(user.humhubPublishName as PublishNameType)
|
||||
this.gradido_address = CONFIG.COMMUNITY_NAME + '/' + user.gradidoID
|
||||
|
||||
this.gradido_address = `${CONFIG.COMMUNITY_NAME}/${
|
||||
publishNameLogic.hasAlias() ? user.alias : user.gradidoID
|
||||
}`
|
||||
}
|
||||
|
||||
firstname: string
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||
/* eslint-disable n/no-process-env */
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import { validate } from '@config/index'
|
||||
import { latestDbVersion } from '@dbTools/config/detectLastDBVersion'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { schema } from './schema'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
Decimal.set({
|
||||
@ -12,16 +17,10 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0086-add_community_location',
|
||||
// DB_VERSION: '0087-add_index_on_user_roles',
|
||||
DB_VERSION: latestDbVersion,
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v23.2024-04-04',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
|
||||
const server = {
|
||||
@ -29,8 +28,11 @@ const server = {
|
||||
JWT_SECRET: process.env.JWT_SECRET ?? 'secret123',
|
||||
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN ?? '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
GDT_ACTIVE: process.env.GDT_ACTIVE === 'true' || false,
|
||||
GDT_API_URL: process.env.GDT_API_URL ?? 'https://gdt.gradido.net',
|
||||
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL ?? 'info',
|
||||
}
|
||||
|
||||
const database = {
|
||||
@ -64,10 +66,9 @@ const dltConnector = {
|
||||
const community = {
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
|
||||
COMMUNITY_URL,
|
||||
COMMUNITY_REGISTER_URL: COMMUNITY_URL + (process.env.COMMUNITY_REGISTER_PATH ?? '/register'),
|
||||
COMMUNITY_REDEEM_URL: COMMUNITY_URL + (process.env.COMMUNITY_REDEEM_PATH ?? '/redeem/{code}'),
|
||||
COMMUNITY_REDEEM_URL: COMMUNITY_URL + (process.env.COMMUNITY_REDEEM_PATH ?? '/redeem/'),
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL:
|
||||
COMMUNITY_URL + (process.env.COMMUNITY_REDEEM_CONTRIBUTION_PATH ?? '/redeem/CL-{code}'),
|
||||
COMMUNITY_URL + (process.env.COMMUNITY_REDEEM_CONTRIBUTION_PATH ?? '/redeem/CL-'),
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION ?? 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL ?? 'support@supportmail.com',
|
||||
@ -76,6 +77,7 @@ const community = {
|
||||
const loginServer = {
|
||||
LOGIN_APP_SECRET: process.env.LOGIN_APP_SECRET ?? '21ffbbc616fe',
|
||||
LOGIN_SERVER_KEY: process.env.LOGIN_SERVER_KEY ?? 'a51ef8ac7ef1abf162fb7a65261acd7a',
|
||||
USE_CRYPTO_WORKER: process.env.USE_CRYPTO_WORKER ?? false,
|
||||
}
|
||||
|
||||
const email = {
|
||||
@ -85,14 +87,14 @@ const email = {
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME ?? '',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER ?? 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD ?? '',
|
||||
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL ?? 'mailserver',
|
||||
EMAIL_SMTP_HOST: process.env.EMAIL_SMTP_HOST ?? 'mailserver',
|
||||
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
|
||||
// eslint-disable-next-line no-unneeded-ternary
|
||||
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
|
||||
EMAIL_LINK_VERIFICATION:
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_VERIFICATION_PATH ?? '/checkEmail/{optin}{code}'),
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_VERIFICATION_PATH ?? '/checkEmail/'),
|
||||
EMAIL_LINK_SETPASSWORD:
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_SETPASSWORD_PATH ?? '/reset-password/{optin}'),
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_SETPASSWORD_PATH ?? '/reset-password/'),
|
||||
EMAIL_LINK_FORGOTPASSWORD:
|
||||
COMMUNITY_URL + (process.env.EMAIL_LINK_FORGOTPASSWORD_PATH ?? '/forgot-password'),
|
||||
EMAIL_LINK_OVERVIEW: COMMUNITY_URL + (process.env.EMAIL_LINK_OVERVIEW_PATH ?? '/overview'),
|
||||
@ -114,18 +116,6 @@ const webhook = {
|
||||
// This is needed by graphql-directive-auth
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
// Check config version
|
||||
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION ?? constants.CONFIG_VERSION.DEFAULT
|
||||
if (
|
||||
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||
constants.CONFIG_VERSION.CURRENT,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
|
||||
)
|
||||
}
|
||||
|
||||
const federation = {
|
||||
FEDERATION_BACKEND_SEND_ON_API: process.env.FEDERATION_BACKEND_SEND_ON_API ?? '1_0',
|
||||
// ?? operator don't work here as expected
|
||||
@ -171,3 +161,5 @@ export const CONFIG = {
|
||||
...gms,
|
||||
...humhub,
|
||||
}
|
||||
|
||||
validate(schema, CONFIG)
|
||||
|
||||
355
backend/src/config/schema.ts
Normal file
355
backend/src/config/schema.ts
Normal file
@ -0,0 +1,355 @@
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import {
|
||||
COMMUNITY_NAME,
|
||||
COMMUNITY_URL,
|
||||
COMMUNITY_DESCRIPTION,
|
||||
COMMUNITY_SUPPORT_MAIL,
|
||||
DB_HOST,
|
||||
DB_PASSWORD,
|
||||
DB_PORT,
|
||||
DB_USER,
|
||||
DB_VERSION,
|
||||
DB_DATABASE,
|
||||
DECAY_START_TIME,
|
||||
GDT_API_URL,
|
||||
GDT_ACTIVE,
|
||||
GMS_ACTIVE,
|
||||
GRAPHIQL,
|
||||
HUMHUB_ACTIVE,
|
||||
LOG4JS_CONFIG,
|
||||
LOGIN_APP_SECRET,
|
||||
LOGIN_SERVER_KEY,
|
||||
LOG_LEVEL,
|
||||
NODE_ENV,
|
||||
PRODUCTION,
|
||||
TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
} from '@config/commonSchema'
|
||||
import Joi from 'joi'
|
||||
|
||||
export const schema = Joi.object({
|
||||
COMMUNITY_NAME,
|
||||
COMMUNITY_URL,
|
||||
COMMUNITY_DESCRIPTION,
|
||||
COMMUNITY_SUPPORT_MAIL,
|
||||
DB_HOST,
|
||||
DB_PASSWORD,
|
||||
DB_PORT,
|
||||
DB_USER,
|
||||
DB_VERSION,
|
||||
DB_DATABASE,
|
||||
DECAY_START_TIME,
|
||||
GDT_API_URL,
|
||||
GDT_ACTIVE,
|
||||
GMS_ACTIVE,
|
||||
GRAPHIQL,
|
||||
HUMHUB_ACTIVE,
|
||||
LOG4JS_CONFIG,
|
||||
LOGIN_APP_SECRET,
|
||||
LOGIN_SERVER_KEY,
|
||||
LOG_LEVEL,
|
||||
NODE_ENV,
|
||||
PRODUCTION,
|
||||
TYPEORM_LOGGING_RELATIVE_PATH,
|
||||
|
||||
COMMUNITY_REDEEM_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.description('The url for redeeming link transactions, must start with frontend base url')
|
||||
.default('http://0.0.0.0/redeem/')
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.required(),
|
||||
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.description(
|
||||
'The url for redeeming contribution link transactions, must start with frontend base url.',
|
||||
)
|
||||
.default('http://0.0.0.0/redeem/CL-')
|
||||
.required(),
|
||||
|
||||
DLT_CONNECTOR: Joi.boolean()
|
||||
.description('Flag to indicate if DLT-Connector is used. (Still in development)')
|
||||
.default(false)
|
||||
.required(),
|
||||
|
||||
DLT_CONNECTOR_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.default('http://localhost:6010')
|
||||
.when('DLT_CONNECTOR', { is: true, then: Joi.required() })
|
||||
.description('The URL for GDT API endpoint'),
|
||||
|
||||
EMAIL: Joi.boolean()
|
||||
.default(false)
|
||||
.description('Enable or disable email functionality')
|
||||
.required(),
|
||||
|
||||
EMAIL_TEST_MODUS: Joi.boolean()
|
||||
.default(false)
|
||||
.description('When enabled, all emails are sended to EMAIL_TEST_RECEIVER')
|
||||
.optional(),
|
||||
|
||||
EMAIL_TEST_RECEIVER: Joi.string()
|
||||
.email()
|
||||
.default('stage1@gradido.net')
|
||||
.when('EMAIL_TEST_MODUS', { is: true, then: Joi.required() })
|
||||
.description('Email address used in test mode'),
|
||||
|
||||
EMAIL_USERNAME: Joi.alternatives().conditional(Joi.ref('EMAIL'), {
|
||||
is: true,
|
||||
then: Joi.alternatives().conditional(Joi.ref('NODE_ENV'), {
|
||||
is: 'development',
|
||||
then: Joi.string()
|
||||
.allow('')
|
||||
.description('Username for SMTP authentication (optional in development)'),
|
||||
otherwise: Joi.string()
|
||||
.pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/)
|
||||
.description('Valid SMTP username required in production')
|
||||
.required(),
|
||||
}),
|
||||
otherwise: Joi.string().allow('').optional(),
|
||||
}),
|
||||
|
||||
EMAIL_SENDER: Joi.string()
|
||||
.email()
|
||||
.when('EMAIL', { is: true, then: Joi.required() })
|
||||
.default('info@gradido.net')
|
||||
.description('Email address used as sender'),
|
||||
|
||||
EMAIL_PASSWORD: Joi.alternatives().conditional(Joi.ref('EMAIL'), {
|
||||
is: true,
|
||||
then: Joi.alternatives().conditional(Joi.ref('NODE_ENV'), {
|
||||
is: 'development',
|
||||
then: Joi.string()
|
||||
.allow('')
|
||||
.description('Password for SMTP authentication (optional in development)'),
|
||||
otherwise: Joi.string()
|
||||
.min(8)
|
||||
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#]).{8,}$/)
|
||||
.description(
|
||||
'Password must be at least 8 characters long, include uppercase and lowercase letters, a number, and a special character',
|
||||
)
|
||||
.required(),
|
||||
}),
|
||||
otherwise: Joi.string().allow('').optional(),
|
||||
}),
|
||||
|
||||
EMAIL_SMTP_HOST: Joi.string()
|
||||
.hostname()
|
||||
.when('EMAIL', { is: true, then: Joi.required() })
|
||||
.default('mailserver')
|
||||
.description('SMTP server hostname'),
|
||||
|
||||
EMAIL_SMTP_PORT: Joi.number()
|
||||
.integer()
|
||||
.positive()
|
||||
.when('EMAIL', { is: true, then: Joi.required() })
|
||||
.default(1025)
|
||||
.description('SMTP server port'),
|
||||
|
||||
EMAIL_TLS: Joi.boolean().default(true).description('Enable or disable TLS for SMTP').optional(),
|
||||
|
||||
EMAIL_LINK_VERIFICATION: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.description('Email Verification link for activate Email.')
|
||||
.required(),
|
||||
|
||||
EMAIL_LINK_SETPASSWORD: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.description('Email Verification link for set initial Password.')
|
||||
.required(),
|
||||
|
||||
EMAIL_LINK_FORGOTPASSWORD: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.description('Email Verification link for set new Password, when old Password was forgotten.')
|
||||
.required(),
|
||||
|
||||
EMAIL_LINK_OVERVIEW: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.custom((value: string, helpers: Joi.CustomHelpers<string>): string | Joi.ErrorReport => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
|
||||
if (!value.startsWith(helpers.state.ancestors[0].COMMUNITY_URL)) {
|
||||
return helpers.error('string.pattern.base', { value, communityUrl: COMMUNITY_URL })
|
||||
}
|
||||
return value
|
||||
})
|
||||
.description('Link to Wallet Overview')
|
||||
.required(),
|
||||
|
||||
EMAIL_CODE_VALID_TIME: Joi.number()
|
||||
.integer()
|
||||
.positive()
|
||||
.max(43200) // max at 30 days
|
||||
.default(1440)
|
||||
.description('Time in minutes a code is valid')
|
||||
.required(),
|
||||
|
||||
EMAIL_CODE_REQUEST_TIME: Joi.number()
|
||||
.integer()
|
||||
.positive()
|
||||
.max(43200) // max at 30 days
|
||||
.default(10)
|
||||
.description('Time in minutes before a new code can be requested')
|
||||
.required(),
|
||||
|
||||
FEDERATION_BACKEND_SEND_ON_API: Joi.string()
|
||||
.pattern(/^\d+_\d+$/)
|
||||
.default('1_0')
|
||||
.description('API Version of sending requests to another communities, e.g., "1_0"')
|
||||
.required(),
|
||||
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER: Joi.number()
|
||||
.integer()
|
||||
.min(1000)
|
||||
.default(60000)
|
||||
.description('Timer interval in milliseconds for community validation')
|
||||
.required(),
|
||||
|
||||
FEDERATION_XCOM_SENDCOINS_ENABLED: Joi.boolean()
|
||||
.default(false)
|
||||
.description('Enable or disable the federation send coins feature')
|
||||
.optional(),
|
||||
|
||||
FEDERATION_XCOM_RECEIVER_COMMUNITY_UUID: Joi.string()
|
||||
.uuid()
|
||||
.default('56a55482-909e-46a4-bfa2-cd025e894ebc')
|
||||
.description(
|
||||
'UUID of the receiver community for federation cross-community transactions if the receiver is unknown',
|
||||
)
|
||||
.required(),
|
||||
|
||||
FEDERATION_XCOM_MAXREPEAT_REVERTSENDCOINS: Joi.number()
|
||||
.integer()
|
||||
.min(0)
|
||||
.default(3)
|
||||
.description('Maximum number of retries for reverting send coins transactions')
|
||||
.required(),
|
||||
|
||||
GMS_CREATE_USER_THROW_ERRORS: Joi.boolean()
|
||||
.default(false)
|
||||
.when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
|
||||
.description('Whether errors should be thrown when creating users in GMS'),
|
||||
|
||||
GMS_API_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
|
||||
.default('http://localhost:4044/')
|
||||
.description('The API URL for the GMS service'),
|
||||
|
||||
GMS_DASHBOARD_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
|
||||
.default('http://localhost:8080/')
|
||||
.description('The URL for the GMS dashboard'),
|
||||
|
||||
GMS_WEBHOOK_SECRET: Joi.string()
|
||||
.min(1)
|
||||
.default('secret')
|
||||
.when('GMS_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
|
||||
.description('The secret postfix for the GMS webhook endpoint'),
|
||||
|
||||
HUMHUB_API_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.when('HUMHUB_ACTIVE', { is: true, then: Joi.required(), otherwise: Joi.optional() })
|
||||
.description('The API URL for HumHub integration'),
|
||||
|
||||
HUMHUB_JWT_KEY: Joi.string()
|
||||
.min(1)
|
||||
.when('HUMHUB_ACTIVE', {
|
||||
is: true,
|
||||
then: Joi.required(),
|
||||
otherwise: Joi.string().allow('').optional(),
|
||||
})
|
||||
.description('JWT key for HumHub integration, must be the same as configured in humhub'),
|
||||
|
||||
PORT: Joi.number()
|
||||
.integer()
|
||||
.min(1024)
|
||||
.max(49151)
|
||||
.description('Port for hosting backend, default: 4000')
|
||||
.default(4000)
|
||||
.required(),
|
||||
|
||||
KLICKTIPP: Joi.boolean()
|
||||
.default(false)
|
||||
.description("Indicates whether Klicktipp integration is enabled, 'true' or 'false'"),
|
||||
|
||||
KLICKTTIPP_API_URL: Joi.string()
|
||||
.uri({ scheme: ['https'] }) // Sicherstellen, dass es eine gültige HTTPS-URL ist
|
||||
.default('https://api.klicktipp.com')
|
||||
.description("The API URL for Klicktipp, must be a valid URL starting with 'https'"),
|
||||
|
||||
KLICKTIPP_USER: Joi.string().default('gradido_test').description('The username for Klicktipp'),
|
||||
|
||||
KLICKTIPP_PASSWORD: Joi.string()
|
||||
.min(6)
|
||||
.default('secret321')
|
||||
.description('The password for Klicktipp, should be at least 6 characters'),
|
||||
|
||||
KLICKTIPP_APIKEY_DE: Joi.string()
|
||||
.default('SomeFakeKeyDE')
|
||||
.description('The API key for Klicktipp (German version)'),
|
||||
|
||||
KLICKTIPP_APIKEY_EN: Joi.string()
|
||||
.default('SomeFakeKeyEN')
|
||||
.description('The API key for Klicktipp (English version)'),
|
||||
|
||||
USE_CRYPTO_WORKER: Joi.boolean()
|
||||
.default(false)
|
||||
.description(
|
||||
'Flag to enable or disable password encryption in separate thread, should be enabled if possible',
|
||||
),
|
||||
|
||||
// TODO: check format
|
||||
JWT_SECRET: Joi.string()
|
||||
.default('secret123')
|
||||
.description('jwt secret for jwt tokens used for login')
|
||||
.required(),
|
||||
|
||||
JWT_EXPIRES_IN: Joi.alternatives()
|
||||
.try(
|
||||
Joi.string()
|
||||
.pattern(/^\d+[smhdw]$/) // Time specification such as “10m”, “1h”, “2d”, etc.
|
||||
.description('Expiration time for JWT login token, in format like "10m", "1h", "1d"')
|
||||
.default('10m'),
|
||||
Joi.number()
|
||||
.positive() // positive number to accept seconds
|
||||
.description('Expiration time for JWT login token in seconds'),
|
||||
)
|
||||
.required()
|
||||
.description('Time for JWT token to expire, auto logout'),
|
||||
|
||||
WEBHOOK_ELOPAGE_SECRET: Joi.string().description("isn't really used any more").optional(),
|
||||
})
|
||||
23
backend/src/data/PublishName.logic.test.ts
Normal file
23
backend/src/data/PublishName.logic.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
import { PublishNameLogic } from './PublishName.logic'
|
||||
|
||||
describe('test publish name logic', () => {
|
||||
describe('test username', () => {
|
||||
it('alias or initials with alias set', () => {
|
||||
const user = new User()
|
||||
user.alias = 'alias'
|
||||
const logic = new PublishNameLogic(user)
|
||||
expect(logic.getUsername(PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS)).toBe(user.alias)
|
||||
})
|
||||
it('alias or initials with empty alias', () => {
|
||||
const user = new User()
|
||||
user.firstName = 'John'
|
||||
user.lastName = 'Smith'
|
||||
const logic = new PublishNameLogic(user)
|
||||
expect(logic.getUsername(PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS)).toBe('JoSm')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,61 +1,91 @@
|
||||
import { User } from '@entity/User'
|
||||
import XRegExp from 'xregexp'
|
||||
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
|
||||
export class PublishNameLogic {
|
||||
// allowed characters for humhub usernames
|
||||
private usernameRegex: RegExp = XRegExp('[\\p{L}\\d_\\-@\\.]', 'g')
|
||||
|
||||
constructor(private user: User) {}
|
||||
|
||||
private firstUpperCaseSecondLowerCase(name: string) {
|
||||
if (name && name.length >= 2) {
|
||||
return name.charAt(0).toUpperCase() + name.charAt(1).toLocaleLowerCase()
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// remove character which are invalid for humhub username
|
||||
private filterOutInvalidChar(name: string) {
|
||||
// eslint-disable-next-line import/no-named-as-default-member
|
||||
return XRegExp.match(name, this.usernameRegex, 'all').join('')
|
||||
}
|
||||
|
||||
public hasAlias(): boolean {
|
||||
if (this.user.alias && this.user.alias.length >= 3) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* get first name based on publishNameType: PublishNameType value
|
||||
* @param publishNameType
|
||||
* @returns user.firstName for PUBLISH_NAME_FIRST, PUBLISH_NAME_FIRST_INITIAL or PUBLISH_NAME_FULL
|
||||
* first initial from user.firstName for PUBLISH_NAME_INITIALS or PUBLISH_NAME_INITIAL_LAST
|
||||
*/
|
||||
public getFirstName(publishNameType: PublishNameType): string {
|
||||
if (
|
||||
[
|
||||
PublishNameType.PUBLISH_NAME_FIRST,
|
||||
PublishNameType.PUBLISH_NAME_FIRST_INITIAL,
|
||||
PublishNameType.PUBLISH_NAME_FULL,
|
||||
].includes(publishNameType)
|
||||
) {
|
||||
return this.user.firstName
|
||||
}
|
||||
if (PublishNameType.PUBLISH_NAME_INITIALS === publishNameType) {
|
||||
return this.user.firstName.substring(0, 1)
|
||||
}
|
||||
if (PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType) {
|
||||
if (this.user.alias) {
|
||||
return this.user.alias
|
||||
} else {
|
||||
return this.user.firstName.substring(0, 1)
|
||||
}
|
||||
}
|
||||
return ''
|
||||
return [
|
||||
PublishNameType.PUBLISH_NAME_FIRST,
|
||||
PublishNameType.PUBLISH_NAME_FIRST_INITIAL,
|
||||
PublishNameType.PUBLISH_NAME_FULL,
|
||||
].includes(publishNameType)
|
||||
? this.user.firstName
|
||||
: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* get last name based on publishNameType: GmsPublishNameType value
|
||||
* @param publishNameType
|
||||
* @returns user.lastName for PUBLISH_NAME_LAST, PUBLISH_NAME_INITIAL_LAST, PUBLISH_NAME_FULL
|
||||
* first initial from user.lastName for PUBLISH_NAME_FIRST_INITIAL, PUBLISH_NAME_INITIALS
|
||||
* @returns user.lastName for PUBLISH_NAME_LAST, PUBLISH_NAME_FULL
|
||||
* first initial from user.lastName for PUBLISH_NAME_FIRST_INITIAL
|
||||
*/
|
||||
public getLastName(publishNameType: PublishNameType): string {
|
||||
if (PublishNameType.PUBLISH_NAME_FULL === publishNameType) {
|
||||
return this.user.lastName
|
||||
} else if (
|
||||
[PublishNameType.PUBLISH_NAME_FIRST_INITIAL, PublishNameType.PUBLISH_NAME_INITIALS].includes(
|
||||
publishNameType,
|
||||
)
|
||||
) {
|
||||
return this.user.lastName.substring(0, 1)
|
||||
} else if (
|
||||
PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType &&
|
||||
!this.user.alias
|
||||
) {
|
||||
return this.user.lastName.substring(0, 1)
|
||||
}
|
||||
return publishNameType === PublishNameType.PUBLISH_NAME_FULL
|
||||
? this.user.lastName
|
||||
: publishNameType === PublishNameType.PUBLISH_NAME_FIRST_INITIAL
|
||||
? this.user.lastName.charAt(0)
|
||||
: ''
|
||||
}
|
||||
|
||||
return ''
|
||||
/**
|
||||
* get username from user.alias for PUBLISH_NAME_ALIAS_OR_INITALS and if user has alias
|
||||
* get first name first two characters and last name first two characters for PUBLISH_NAME_ALIAS_OR_INITALS
|
||||
* if no alias or PUBLISH_NAME_INITIALS
|
||||
* @param publishNameType
|
||||
* @returns user.alias for publishNameType = PUBLISH_NAME_ALIAS_OR_INITALS and user has alias
|
||||
* else return user.firstName[0,2] + user.lastName[0,2] for publishNameType = [PUBLISH_NAME_ALIAS_OR_INITALS, PUBLISH_NAME_INITIALS]
|
||||
*/
|
||||
public getUsername(publishNameType: PublishNameType): string {
|
||||
if (this.isUsernameFromInitials(publishNameType)) {
|
||||
return (
|
||||
this.firstUpperCaseSecondLowerCase(this.filterOutInvalidChar(this.user.firstName)) +
|
||||
this.firstUpperCaseSecondLowerCase(this.filterOutInvalidChar(this.user.lastName))
|
||||
)
|
||||
} else if (this.isUsernameFromAlias(publishNameType)) {
|
||||
return this.filterOutInvalidChar(this.user.alias)
|
||||
}
|
||||
return this.user.gradidoID
|
||||
}
|
||||
|
||||
public isUsernameFromInitials(publishNameType: PublishNameType): boolean {
|
||||
return (
|
||||
PublishNameType.PUBLISH_NAME_INITIALS === publishNameType ||
|
||||
(PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType && !this.hasAlias())
|
||||
)
|
||||
}
|
||||
|
||||
public isUsernameFromAlias(publishNameType: PublishNameType): boolean {
|
||||
return PublishNameType.PUBLISH_NAME_ALIAS_OR_INITALS === publishNameType && this.hasAlias()
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,6 +124,10 @@ exports[`sendEmailVariants sendAccountActivationEmail result has the correct htm
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -160,7 +164,8 @@ If the validity of the link has already expired, you can have a new link sent to
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -295,6 +300,10 @@ exports[`sendEmailVariants sendAccountMultiRegistrationEmail calls "sendEmailTra
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -329,7 +338,8 @@ exports[`sendEmailVariants sendAccountMultiRegistrationEmail calls "sendEmailTra
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -464,6 +474,10 @@ exports[`sendEmailVariants sendAddedContributionMessageEmail result has the corr
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -495,7 +509,8 @@ exports[`sendEmailVariants sendAddedContributionMessageEmail result has the corr
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -630,6 +645,10 @@ exports[`sendEmailVariants sendContributionChangedByModeratorEmail result has th
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -662,7 +681,8 @@ exports[`sendEmailVariants sendContributionChangedByModeratorEmail result has th
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -797,6 +817,10 @@ exports[`sendEmailVariants sendContributionConfirmedEmail result has the correct
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -829,7 +853,8 @@ exports[`sendEmailVariants sendContributionConfirmedEmail result has the correct
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -964,6 +989,10 @@ exports[`sendEmailVariants sendContributionDeletedEmail result has the correct h
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -996,7 +1025,8 @@ exports[`sendEmailVariants sendContributionDeletedEmail result has the correct h
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -1131,6 +1161,10 @@ exports[`sendEmailVariants sendContributionDeniedEmail result has the correct ht
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -1163,7 +1197,8 @@ exports[`sendEmailVariants sendContributionDeniedEmail result has the correct ht
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -1298,6 +1333,10 @@ exports[`sendEmailVariants sendResetPasswordEmail result has the correct html as
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -1334,7 +1373,8 @@ If the validity of the link has already expired, you can have a new link sent to
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -1469,6 +1509,10 @@ exports[`sendEmailVariants sendTransactionLinkRedeemedEmail result has the corre
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -1501,7 +1545,8 @@ exports[`sendEmailVariants sendTransactionLinkRedeemedEmail result has the corre
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
@ -1636,6 +1681,10 @@ exports[`sendEmailVariants sendTransactionReceivedEmail result has the correct h
|
||||
line-break: anywhere;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
.slink {
|
||||
width: 150px;
|
||||
}
|
||||
@ -1650,13 +1699,17 @@ exports[`sendEmailVariants sendTransactionReceivedEmail result has the correct h
|
||||
<h2 style=\\"margin-top: 15px; color: #383838;\\">Bibi Bloxberg has sent you 37.40 Gradido</h2>
|
||||
<div class=\\"text-block\\" style=\\"margin-top: 20px; color: #9ca0a8;\\">
|
||||
<p>Hello Peter Lustig,</p>
|
||||
<p>You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).</p>
|
||||
<p> You have just received 37.40 GDD from Bibi Bloxberg (<a href=\\"mailto:bibi@bloxberg.de?subject=RE%3A%20Bibi%20Bloxberg%20has%20sent%20you%2037.40%20Gradido\\">bibi@bloxberg.de</a>).
|
||||
</p>
|
||||
</div>
|
||||
<div class=\\"content\\" style=\\"display: block; width: 78%; margin: 40px 1% 40px 1%; padding: 20px 10% 40px 10%; border-radius: 24px; background-image: linear-gradient(180deg, #f5f5f5, #f5f5f5);\\">
|
||||
<h2 style=\\"margin-top: 15px; color: #383838;\\">Transaction details</h2>
|
||||
<div class=\\"p_content\\" style=\\"margin: 15px 0 15px 0; line-height: 26px; color: #9ca0a8;\\">You can find transaction details in your Gradido account.</div><a class=\\"button-3\\" href=\\"http://localhost/transactions\\" style=\\"display: inline-block; padding: 9px 15px; color: white; border: 0; line-height: inherit; text-decoration: none; cursor: pointer; border-radius: 20px; background-image: radial-gradient(circle farthest-corner at 0% 0%, #f9cd69, #c58d38); box-shadow: 16px 13px 35px 0 rgba(56, 56, 56, 0.3); margin: 25px 0 25px 0; width: 50%;\\">To account</a>
|
||||
<div class=\\"p_content\\" style=\\"margin: 15px 0 15px 0; line-height: 26px; color: #9ca0a8;\\">Please do not reply to this email.</div>
|
||||
</div>
|
||||
<h2 style=\\"margin-top: 15px; color: #383838;\\">Message</h2>
|
||||
<div class=\\"child-left\\" style=\\"text-align: left;\\">
|
||||
<div class=\\"p_content\\" style=\\"margin: 15px 0 15px 0; line-height: 26px; color: #9ca0a8;\\">Du bist schon lustiger ;)</div>
|
||||
</div>
|
||||
<div class=\\"child-right\\" style=\\"text-align: right;\\"><a class=\\"button-5\\" href=\\"mailto:bibi@bloxberg.de?subject=RE%3A%20Bibi%20Bloxberg%20has%20sent%20you%2037.40%20Gradido\\" style=\\"display: inline-block; padding: 9px 15px; border: 0; line-height: inherit; text-decoration: none; cursor: pointer; border-radius: 20px; background-image: radial-gradient(circle farthest-corner at 0% 0%, #f9cd69, #c58d38); margin: 25px 0 25px 0; background: linear-gradient(135deg, #53900c, #6e6e6e); font-size: 20px; font-weight: 600; color: #f5f5f5; width: auto; box-shadow: 20px 20px 25px; transition: all 0.3s ease;\\"><span class=\\"chatbox-wrapper\\" style=\\"margin-right: 8px;\\"><img class=\\"bi-chatbox\\" alt=\\"chatbox\\" loading=\\"lazy\\" src=\\"cid:chatboxicon\\" style=\\"margin-bottom: -5px;\\"></span><span>Reply</span></a>
|
||||
</div>
|
||||
</div><a class=\\"button-3\\" href=\\"http://localhost/transactions\\" style=\\"display: inline-block; padding: 9px 15px; color: white; border: 0; line-height: inherit; text-decoration: none; cursor: pointer; border-radius: 20px; background-image: radial-gradient(circle farthest-corner at 0% 0%, #f9cd69, #c58d38); box-shadow: 16px 13px 35px 0 rgba(56, 56, 56, 0.3); margin: 25px 0 25px 0; width: 50%;\\">To account</a>
|
||||
<div class=\\"text-block\\" style=\\"margin-top: 20px; color: #9ca0a8;\\">
|
||||
<p>Kind regards,<br>your Gradido team
|
||||
</p>
|
||||
@ -1667,7 +1720,8 @@ exports[`sendEmailVariants sendTransactionReceivedEmail result has the correct h
|
||||
<div class=\\"socialmedia\\" style=\\"display: flex; margin-top: 40px; max-width: 600px;\\"><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.facebook.com/groups/Gradido/\\" style=\\"width: 150px;\\"><img class=\\"bi-facebook\\" alt=\\"facebook\\" loading=\\"lazy\\" src=\\"cid:facebookicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://t.me/GradidoGruppe\\" style=\\"width: 150px;\\"><img class=\\"bi-telegram\\" alt=\\"Telegram\\" loading=\\"lazy\\" src=\\"cid:telegramicon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://twitter.com/gradido\\" style=\\"width: 150px;\\"><img class=\\"bi-twitter\\" alt=\\"Twitter\\" loading=\\"lazy\\" src=\\"cid:twittericon\\"></a><a class=\\"slink\\" target=\\"_blank\\" href=\\"https://www.youtube.com/c/GradidoNet\\" style=\\"width: 150px;\\"><img class=\\"bi-youtube\\" alt=\\"youtube\\" loading=\\"lazy\\" src=\\"cid:youtubeicon\\"></a></div>
|
||||
<div class=\\"line\\" style=\\"width: 100%; height: 13px; margin-top: 40px; background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);\\"></div>
|
||||
<div class=\\"footer\\" style=\\"padding-bottom: 20px;\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a><img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\">
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">If you have any further questions, please contact our support.</div><a class=\\"footer_p2\\" href=\\"mailto:support@gradido.net\\" style=\\"color: #383838; font-weight: bold;\\">support@gradido.net</a>
|
||||
<div> <img class=\\"image\\" alt=\\"Gradido Logo\\" src=\\"https://gdd.gradido.net/img/brand/green.png\\" style=\\"width: 200px; margin-top: 30px; margin-bottom: 30px;\\" width=\\"200\\"></div>
|
||||
<div><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/impressum/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Impressum</a></div><br><a class=\\"terms_of_use\\" href=\\"https://gradido.net/de/datenschutz/\\" target=\\"_blank\\" style=\\"color: #9ca0a8;\\">Privacy Policy</a>
|
||||
<div class=\\"footer_p1\\" style=\\"margin-top: 30px; color: #9ca0a8; margin-bottom: 30px;\\">Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><br><br></div>
|
||||
</div>
|
||||
|
||||
@ -7,9 +7,12 @@ import { CONFIG } from '@/config'
|
||||
|
||||
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||
|
||||
const testMailServerHost = 'localhost'
|
||||
const testMailServerPort = 1025
|
||||
|
||||
CONFIG.EMAIL = false
|
||||
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
|
||||
CONFIG.EMAIL_SMTP_PORT = 1234
|
||||
CONFIG.EMAIL_SMTP_HOST = testMailServerHost
|
||||
CONFIG.EMAIL_SMTP_PORT = testMailServerPort
|
||||
CONFIG.EMAIL_SENDER = 'info@gradido.net'
|
||||
CONFIG.EMAIL_USERNAME = 'user'
|
||||
CONFIG.EMAIL_PASSWORD = 'pwd'
|
||||
@ -20,11 +23,11 @@ jest.mock('nodemailer', () => {
|
||||
__esModule: true,
|
||||
createTransport: jest.fn(() => {
|
||||
return {
|
||||
sendMail: jest.fn(() => {
|
||||
sendMail: () => {
|
||||
return {
|
||||
messageId: 'message',
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
@ -73,8 +76,8 @@ describe('sendEmailTranslated', () => {
|
||||
|
||||
it('calls the transporter', () => {
|
||||
expect(createTransport).toBeCalledWith({
|
||||
host: 'EMAIL_SMTP_URL',
|
||||
port: 1234,
|
||||
host: testMailServerHost,
|
||||
port: testMailServerPort,
|
||||
secure: false,
|
||||
requireTLS: true,
|
||||
auth: {
|
||||
@ -87,11 +90,6 @@ describe('sendEmailTranslated', () => {
|
||||
describe('call of "sendEmailTranslated"', () => {
|
||||
it('has expected result', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['receiver@mail.org', 'support@gradido.net'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
@ -135,11 +133,6 @@ describe('sendEmailTranslated', () => {
|
||||
|
||||
it('call of "sendEmailTranslated" with faked "to"', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: CONFIG.EMAIL_SENDER,
|
||||
to: [CONFIG.EMAIL_TEST_RECEIVER, 'support@gradido.net'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: CONFIG.EMAIL_TEST_RECEIVER,
|
||||
cc: 'support@gradido.net',
|
||||
|
||||
@ -45,7 +45,7 @@ export const sendEmailTranslated = async ({
|
||||
receiver.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||
}
|
||||
const transport = createTransport({
|
||||
host: CONFIG.EMAIL_SMTP_URL,
|
||||
host: CONFIG.EMAIL_SMTP_HOST,
|
||||
port: CONFIG.EMAIL_SMTP_PORT,
|
||||
secure: false, // true for 465, false for other ports
|
||||
requireTLS: CONFIG.EMAIL_TLS,
|
||||
@ -62,6 +62,7 @@ export const sendEmailTranslated = async ({
|
||||
message: {
|
||||
from: `Gradido (${i18n.__('emails.general.doNotAnswer')}) <${CONFIG.EMAIL_SENDER}>`,
|
||||
},
|
||||
send: CONFIG.EMAIL,
|
||||
transport,
|
||||
preview: false,
|
||||
// i18n, // is only needed if you don't install i18n
|
||||
@ -98,13 +99,18 @@ export const sendEmailTranslated = async ({
|
||||
path: path.join(__dirname, 'templates/includes/youtube-icon.png'),
|
||||
cid: 'youtubeicon',
|
||||
},
|
||||
{
|
||||
filename: 'chatbox-icon.png',
|
||||
path: path.join(__dirname, 'templates/includes/chatbox-icon.png'),
|
||||
cid: 'chatboxicon',
|
||||
},
|
||||
],
|
||||
},
|
||||
locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
logger.error('Error sending notification email', error)
|
||||
return false
|
||||
return error
|
||||
})
|
||||
|
||||
return resultSend
|
||||
|
||||
@ -11,7 +11,8 @@ import { logger, i18n as localization } from '@test/testSetup'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { sendEmailTranslated } from './sendEmailTranslated'
|
||||
// eslint-disable-next-line import/no-namespace
|
||||
import * as sendEmailTranslatedApi from './sendEmailTranslated'
|
||||
import {
|
||||
sendAddedContributionMessageEmail,
|
||||
sendAccountActivationEmail,
|
||||
@ -25,7 +26,29 @@ import {
|
||||
sendContributionChangedByModeratorEmail,
|
||||
} from './sendEmailVariants'
|
||||
|
||||
const testMailServerHost = 'localhost'
|
||||
const testMailServerPort = 1025
|
||||
const testMailTLS = false
|
||||
|
||||
CONFIG.EMAIL_SENDER = 'info@gradido.net'
|
||||
CONFIG.EMAIL_SMTP_HOST = testMailServerHost
|
||||
CONFIG.EMAIL_SMTP_PORT = testMailServerPort
|
||||
CONFIG.EMAIL_TLS = testMailTLS
|
||||
|
||||
jest.mock('nodemailer', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
createTransport: jest.fn(() => {
|
||||
return {
|
||||
sendMail: () => {
|
||||
return {
|
||||
messageId: 'message',
|
||||
}
|
||||
},
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
let con: Connection
|
||||
let testEnv: {
|
||||
@ -43,13 +66,7 @@ afterAll(async () => {
|
||||
await con.close()
|
||||
})
|
||||
|
||||
jest.mock('./sendEmailTranslated', () => {
|
||||
const originalModule = jest.requireActual('./sendEmailTranslated')
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEmailTranslated: jest.fn((a) => originalModule.sendEmailTranslated(a)),
|
||||
}
|
||||
})
|
||||
const sendEmailTranslatedSpy = jest.spyOn(sendEmailTranslatedApi, 'sendEmailTranslated')
|
||||
|
||||
describe('sendEmailVariants', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -70,7 +87,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -93,11 +110,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -129,7 +141,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -151,11 +163,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -185,7 +192,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -204,11 +211,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -243,7 +245,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -267,11 +269,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -305,7 +302,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -329,11 +326,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -366,7 +358,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -389,11 +381,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('has expected result', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -426,7 +413,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -449,11 +436,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -485,7 +467,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -507,11 +489,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -546,7 +523,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -571,11 +548,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
@ -600,6 +572,7 @@ describe('sendEmailVariants', () => {
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
language: 'en',
|
||||
memo: 'Du bist schon lustiger ;)',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
@ -609,7 +582,7 @@ describe('sendEmailVariants', () => {
|
||||
|
||||
describe('calls "sendEmailTranslated"', () => {
|
||||
it('with expected parameters', () => {
|
||||
expect(sendEmailTranslated).toBeCalledWith({
|
||||
expect(sendEmailTranslatedSpy).toBeCalledWith({
|
||||
receiver: {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
},
|
||||
@ -618,6 +591,7 @@ describe('sendEmailVariants', () => {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
memo: 'Du bist schon lustiger ;)',
|
||||
senderFirstName: 'Bibi',
|
||||
senderLastName: 'Bloxberg',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
@ -633,11 +607,6 @@ describe('sendEmailVariants', () => {
|
||||
describe('result', () => {
|
||||
it('is the expected object', () => {
|
||||
expect(result).toMatchObject({
|
||||
envelope: {
|
||||
from: 'info@gradido.net',
|
||||
to: ['peter@lustig.de'],
|
||||
},
|
||||
message: expect.any(String),
|
||||
originalMessage: expect.objectContaining({
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
|
||||
|
||||
@ -247,6 +247,7 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
senderEmail: string
|
||||
memo: string
|
||||
transactionAmount: Decimal
|
||||
}): Promise<Record<string, unknown> | boolean | null> => {
|
||||
return sendEmailTranslated({
|
||||
@ -256,6 +257,7 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
memo: data.memo,
|
||||
senderFirstName: data.senderFirstName,
|
||||
senderLastName: data.senderLastName,
|
||||
senderEmail: data.senderEmail,
|
||||
|
||||
1
backend/src/emails/templates/includes/Chatbox.svg
Normal file
1
backend/src/emails/templates/includes/Chatbox.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e8eaed"><path d="M240-400h480v-80H240v80Zm0-120h480v-80H240v80Zm0-120h480v-80H240v80ZM880-80 720-240H160q-33 0-56.5-23.5T80-320v-480q0-33 23.5-56.5T160-880h640q33 0 56.5 23.5T880-800v720ZM160-320h594l46 45v-525H160v480Zm0 0v-480 480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 341 B |
22
backend/src/emails/templates/includes/answear_button.svg
Normal file
22
backend/src/emails/templates/includes/answear_button.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="386.729" height="196.289" viewBox="0 0 386.729 196.289">
|
||||
<defs>
|
||||
<linearGradient id="linear-gradient" x1="0.552" y1="-1.481" x2="0.673" y2="1.111" gradientUnits="objectBoundingBox">
|
||||
<stop offset="0" stop-color="#53900c"/>
|
||||
<stop offset="1" stop-color="#6e6e6e"/>
|
||||
</linearGradient>
|
||||
<filter id="bg_button_copy" x="0" y="0" width="386.729" height="196.289" filterUnits="userSpaceOnUse">
|
||||
<feOffset dx="20" dy="20" input="SourceAlpha"/>
|
||||
<feGaussianBlur stdDeviation="25" result="blur"/>
|
||||
<feFlood flood-color="#383838" flood-opacity="0.306"/>
|
||||
<feComposite operator="in" in2="blur"/>
|
||||
<feComposite in="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g id="Antwort_button" transform="translate(55 55)">
|
||||
<g transform="matrix(1, 0, 0, 1, -55, -55)" filter="url(#bg_button_copy)">
|
||||
<path id="bg_button_copy-2" data-name="bg button copy" d="M22.44,0H214.289a22.44,22.44,0,0,1,22.44,22.44v1.408a22.44,22.44,0,0,1-22.44,22.44H22.44A22.44,22.44,0,0,1,0,23.848V22.44A22.44,22.44,0,0,1,22.44,0Z" transform="translate(55 55)" fill="url(#linear-gradient)"/>
|
||||
</g>
|
||||
<text id="Jetzt_antworten" data-name="Jetzt antworten" transform="translate(130.252 30.144)" fill="#f5f5f5" font-size="20" font-family="WorkSans-SemiBold, Work Sans" font-weight="600"><tspan x="-79.86" y="0">Jetzt antworten</tspan></text>
|
||||
<path id="Icon_answer" data-name="Icon answer" d="M83.2-870.4h9.6V-872H83.2Zm0-2.4h9.6v-1.6H83.2Zm0-2.4h9.6v-1.6H83.2ZM96-864l-3.2-3.2H81.6a1.541,1.541,0,0,1-1.13-.47A1.541,1.541,0,0,1,80-868.8v-9.6a1.541,1.541,0,0,1,.47-1.13A1.541,1.541,0,0,1,81.6-880H94.4a1.541,1.541,0,0,1,1.13.47A1.541,1.541,0,0,1,96-878.4Zm-14.4-4.8H93.48l.92.9v-10.5H81.6Zm0,0v0Z" transform="translate(-59.497 895.145)" fill="#e8eaed"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
backend/src/emails/templates/includes/chatbox-icon.png
Normal file
BIN
backend/src/emails/templates/includes/chatbox-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 323 B |
6
backend/src/emails/templates/includes/chatbox-icon.pug
Normal file
6
backend/src/emails/templates/includes/chatbox-icon.pug
Normal file
@ -0,0 +1,6 @@
|
||||
span.chatbox-wrapper
|
||||
img.bi-chatbox(
|
||||
alt="chatbox"
|
||||
loading="lazy"
|
||||
src="cid:chatboxicon"
|
||||
)
|
||||
@ -44,9 +44,10 @@ footer
|
||||
class="footer_p2"
|
||||
href='mailto:' + t("emails.footer.supportEmail")
|
||||
)= t("emails.footer.supportEmail")
|
||||
img.image(
|
||||
alt="Gradido Logo"
|
||||
src="https://gdd.gradido.net/img/brand/green.png"
|
||||
div
|
||||
img.image(
|
||||
alt="Gradido Logo"
|
||||
src="https://gdd.gradido.net/img/brand/green.png"
|
||||
)
|
||||
div
|
||||
a(
|
||||
|
||||
@ -51,7 +51,8 @@ h2 {
|
||||
}
|
||||
|
||||
.button-3,
|
||||
.button-4 {
|
||||
.button-4,
|
||||
.button-5 {
|
||||
display: inline-block;
|
||||
padding: 9px 15px;
|
||||
color: white;
|
||||
@ -70,6 +71,35 @@ h2 {
|
||||
background-image: radial-gradient(circle farthest-corner at 0% 0%, #616161, #c2c2c2);
|
||||
}
|
||||
|
||||
.button-5 {
|
||||
background: linear-gradient(135deg, #53900c, #6e6e6e);
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #f5f5f5;
|
||||
width: auto;
|
||||
box-shadow: 20px 20px 25px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.button-5:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 20px 25px 30px rgba(56, 56, 56, 0.4);
|
||||
}
|
||||
|
||||
.chatbox-wrapper {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.bi-chatbox {
|
||||
margin-bottom: -5px;
|
||||
}
|
||||
|
||||
.child-right {
|
||||
text-align: right;
|
||||
}
|
||||
.child-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.socialmedia {
|
||||
display: flex;
|
||||
margin-top: 40px;
|
||||
|
||||
@ -1,15 +1,30 @@
|
||||
extend ../layout.pug
|
||||
|
||||
block content
|
||||
block content
|
||||
mixin mailto(email, subject)
|
||||
- var formattedSubject = encodeURIComponent(subject)
|
||||
a(class!=attributes.class href=`mailto:${email}?subject=${formattedSubject}`)
|
||||
block
|
||||
|
||||
- var subject= t('emails.transactionReceived.replySubject', { senderFirstName, senderLastName, transactionAmount })
|
||||
h2= t('emails.transactionReceived.title', { senderFirstName, senderLastName, transactionAmount })
|
||||
.text-block
|
||||
include ../includes/salutation.pug
|
||||
p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail })
|
||||
p
|
||||
= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName })
|
||||
| (
|
||||
+mailto(senderEmail, subject)=senderEmail
|
||||
|).
|
||||
.content
|
||||
h2= t('emails.general.transactionDetails')
|
||||
div(class="p_content")= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
h2= t('emails.general.message')
|
||||
.child-left
|
||||
div(class="p_content")= memo
|
||||
.child-right
|
||||
+mailto(senderEmail, subject)(class="button-5")
|
||||
include ../includes/chatbox-icon.pug
|
||||
span #{t('emails.general.answerNow')}
|
||||
|
||||
a.button-3(href=`${communityURL}/transactions`) #{t('emails.general.toAccount')}
|
||||
a.button-3(href=`${communityURL}/transactions`) #{t('emails.general.toAccount')}
|
||||
|
||||
include ../includes/doNotReply.pug
|
||||
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ export class ContributionLink {
|
||||
this.cycle = contributionLink.cycle
|
||||
this.maxPerCycle = contributionLink.maxPerCycle
|
||||
this.code = contributionLink.code
|
||||
this.link = CONFIG.COMMUNITY_REDEEM_CONTRIBUTION_URL.replace(/{code}/g, this.code)
|
||||
this.link = CONFIG.COMMUNITY_REDEEM_CONTRIBUTION_URL + this.code
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
|
||||
@ -20,7 +20,7 @@ export class TransactionLink {
|
||||
this.deletedAt = transactionLink.deletedAt
|
||||
this.redeemedAt = transactionLink.redeemedAt
|
||||
this.redeemedBy = redeemedBy
|
||||
this.link = CONFIG.COMMUNITY_REDEEM_URL.replace(/{code}/g, this.code)
|
||||
this.link = CONFIG.COMMUNITY_REDEEM_URL + this.code
|
||||
}
|
||||
|
||||
@Field(() => Int)
|
||||
|
||||
12
backend/src/graphql/model/UserLocationResult.ts
Normal file
12
backend/src/graphql/model/UserLocationResult.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
import { Location } from './Location'
|
||||
|
||||
@ObjectType()
|
||||
export class UserLocationResult {
|
||||
@Field(() => Location)
|
||||
userLocation: Location
|
||||
|
||||
@Field(() => Location)
|
||||
communityLocation: Location
|
||||
}
|
||||
@ -28,6 +28,8 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
import { getCommunityByUuid } from './util/communities'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
// to do: We need a setup for the tests that closes the connection
|
||||
let mutate: ApolloServerTestClient['mutate'],
|
||||
query: ApolloServerTestClient['query'],
|
||||
|
||||
@ -22,6 +22,8 @@ import { listContributionLinks } from '@/seeds/graphql/queries'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'],
|
||||
query: ApolloServerTestClient['query'],
|
||||
con: Connection
|
||||
|
||||
@ -27,6 +27,7 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
return {
|
||||
|
||||
@ -59,6 +59,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { getFirstDayOfPreviousNMonth } from '@/util/utilities'
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants')
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'],
|
||||
query: ApolloServerTestClient['query'],
|
||||
|
||||
@ -24,6 +24,16 @@ export class GdtResolver {
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
@Ctx() context: Context,
|
||||
): Promise<GdtEntryList> {
|
||||
if (!CONFIG.GDT_ACTIVE) {
|
||||
return new GdtEntryList(
|
||||
'disabled',
|
||||
0,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
|
||||
[],
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
const userEntity = getUser(context)
|
||||
|
||||
try {
|
||||
@ -51,6 +61,9 @@ export class GdtResolver {
|
||||
@Authorized([RIGHTS.GDT_BALANCE])
|
||||
@Query(() => Float, { nullable: true })
|
||||
async gdtBalance(@Ctx() context: Context): Promise<number | null> {
|
||||
if (!CONFIG.GDT_ACTIVE) {
|
||||
return null
|
||||
}
|
||||
const user = getUser(context)
|
||||
try {
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
@ -71,6 +84,9 @@ export class GdtResolver {
|
||||
@Query(() => Int)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async existPid(@Arg('pid', () => Int) pid: number): Promise<number> {
|
||||
if (!CONFIG.GDT_ACTIVE) {
|
||||
return 0
|
||||
}
|
||||
// load user
|
||||
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
|
||||
if (!resultPID.success) {
|
||||
|
||||
@ -15,6 +15,8 @@ import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let testEnv: any, mutate: any, con: any
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
@ -38,6 +38,8 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
|
||||
import { transactionLinkCode } from './TransactionLinkResolver'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
// mock semaphore to allow use fake timers
|
||||
jest.mock('@/util/TRANSACTIONS_LOCK')
|
||||
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||
|
||||
@ -37,6 +37,8 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'], con: Connection
|
||||
let query: ApolloServerTestClient['query']
|
||||
|
||||
|
||||
@ -190,6 +190,7 @@ export const executeTransaction = async (
|
||||
lastName: recipient.lastName,
|
||||
email: recipient.emailContact.email,
|
||||
language: recipient.language,
|
||||
memo,
|
||||
senderFirstName: sender.firstName,
|
||||
senderLastName: sender.lastName,
|
||||
senderEmail: sender.emailContact.email,
|
||||
@ -230,8 +231,11 @@ export class TransactionResolver {
|
||||
logger.addContext('user', user.id)
|
||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||
|
||||
const gdtResolver = new GdtResolver()
|
||||
const balanceGDTPromise = gdtResolver.gdtBalance(context)
|
||||
let balanceGDTPromise: Promise<number | null> = Promise.resolve(null)
|
||||
if (CONFIG.GDT_ACTIVE) {
|
||||
const gdtResolver = new GdtResolver()
|
||||
balanceGDTPromise = gdtResolver.gdtBalance(context)
|
||||
}
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await getLastTransaction(user.id)
|
||||
@ -418,8 +422,10 @@ export class TransactionResolver {
|
||||
).toDecimalPlaces(2, Decimal.ROUND_HALF_UP)
|
||||
}
|
||||
})
|
||||
const balanceGDT = await balanceGDTPromise
|
||||
context.balanceGDT = balanceGDT
|
||||
if (CONFIG.GDT_ACTIVE) {
|
||||
const balanceGDT = await balanceGDTPromise
|
||||
context.balanceGDT = balanceGDT
|
||||
}
|
||||
|
||||
// Construct Result
|
||||
return new TransactionList(await balanceResolver.balance(context), transactions)
|
||||
|
||||
@ -74,6 +74,7 @@ import { objectValuesToArray } from '@/util/utilities'
|
||||
import { Location2Point } from './util/Location2Point'
|
||||
|
||||
jest.mock('@/apis/humhub/HumHubClient')
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
@ -96,6 +97,8 @@ jest.mock('@/apis/KlicktippController', () => {
|
||||
}
|
||||
})
|
||||
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = 10
|
||||
|
||||
let admin: User
|
||||
let user: User
|
||||
let mutate: ApolloServerTestClient['mutate'],
|
||||
@ -112,6 +115,7 @@ beforeAll(async () => {
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
CONFIG.HUMHUB_ACTIVE = false
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
@ -185,7 +189,7 @@ describe('UserResolver', () => {
|
||||
communityUuid: homeCom.communityUuid,
|
||||
foreign: false,
|
||||
gmsAllowed: true,
|
||||
humhubAllowed: false,
|
||||
humhubAllowed: true,
|
||||
gmsPublishName: 0,
|
||||
humhubPublishName: 0,
|
||||
gmsPublishLocation: 2,
|
||||
@ -240,10 +244,10 @@ describe('UserResolver', () => {
|
||||
|
||||
describe('account activation email', () => {
|
||||
it('sends an account activation email', () => {
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
emailVerificationCode,
|
||||
).replace(/{code}/g, '')
|
||||
const activationLink = `${
|
||||
CONFIG.EMAIL_LINK_VERIFICATION
|
||||
}${emailVerificationCode.toString()}`
|
||||
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
@ -587,8 +591,8 @@ describe('UserResolver', () => {
|
||||
expect(newUser.emailContact.emailChecked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('updates the password', () => {
|
||||
const encryptedPass = encryptPassword(newUser, 'Aa12345_')
|
||||
it('updates the password', async () => {
|
||||
const encryptedPass = await encryptPassword(newUser, 'Aa12345_')
|
||||
expect(newUser.password.toString()).toEqual(encryptedPass.toString())
|
||||
})
|
||||
|
||||
@ -1546,9 +1550,9 @@ describe('UserResolver', () => {
|
||||
|
||||
expect(bibi).toEqual(
|
||||
expect.objectContaining({
|
||||
password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
|
||||
.readBigUInt64LE()
|
||||
.toString(),
|
||||
password: (
|
||||
await SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')
|
||||
).toString(),
|
||||
passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
|
||||
}),
|
||||
)
|
||||
@ -1570,10 +1574,7 @@ describe('UserResolver', () => {
|
||||
})
|
||||
bibi = usercontact.user
|
||||
bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL
|
||||
bibi.password = SecretKeyCryptographyCreateKey(
|
||||
'bibi@bloxberg.de',
|
||||
'Aa12345_',
|
||||
)[0].readBigUInt64LE()
|
||||
bibi.password = await SecretKeyCryptographyCreateKey('bibi@bloxberg.de', 'Aa12345_')
|
||||
|
||||
await bibi.save()
|
||||
})
|
||||
@ -1590,9 +1591,9 @@ describe('UserResolver', () => {
|
||||
expect(bibi).toEqual(
|
||||
expect.objectContaining({
|
||||
firstName: 'Bibi',
|
||||
password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
|
||||
.readBigUInt64LE()
|
||||
.toString(),
|
||||
password: (
|
||||
await SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')
|
||||
).toString(),
|
||||
passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
|
||||
}),
|
||||
)
|
||||
@ -2229,14 +2230,13 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('sends an account activation email', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
const userContact = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
userConatct.emailVerificationCode.toString(),
|
||||
).replace(/{code}/g, '')
|
||||
const activationLink = `${
|
||||
CONFIG.EMAIL_LINK_VERIFICATION
|
||||
}${userContact.emailVerificationCode.toString()}`
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
@ -2251,14 +2251,14 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the EMAIL_ADMIN_CONFIRMATION event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
const userContact = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_ADMIN_CONFIRMATION,
|
||||
affectedUserId: userConatct.user.id,
|
||||
affectedUserId: userContact.user.id,
|
||||
actingUserId: admin.id,
|
||||
}),
|
||||
)
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { getConnection, In } from '@dbTools/typeorm'
|
||||
import { getConnection, In, Point } from '@dbTools/typeorm'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { UserRole } from '@entity/UserRole'
|
||||
import i18n from 'i18n'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql'
|
||||
import { IRestResponse } from 'typed-rest-client'
|
||||
@ -29,15 +28,16 @@ import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
import { GmsUserAuthenticationResult } from '@model/GmsUserAuthenticationResult'
|
||||
import { User } from '@model/User'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { UserLocationResult } from '@model/UserLocationResult'
|
||||
|
||||
import { updateGmsUser } from '@/apis/gms/GmsClient'
|
||||
import { GmsUser } from '@/apis/gms/model/GmsUser'
|
||||
import { HumHubClient } from '@/apis/humhub/HumHubClient'
|
||||
import { GetUser } from '@/apis/humhub/model/GetUser'
|
||||
import { PostUser } from '@/apis/humhub/model/PostUser'
|
||||
import { subscribe } from '@/apis/KlicktippController'
|
||||
import { encode } from '@/auth/JWT'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { CONFIG } from '@/config'
|
||||
import { PublishNameLogic } from '@/data/PublishName.logic'
|
||||
import {
|
||||
sendAccountActivationEmail,
|
||||
sendAccountMultiRegistrationEmail,
|
||||
@ -59,6 +59,7 @@ import {
|
||||
EVENT_ADMIN_USER_DELETE,
|
||||
EVENT_ADMIN_USER_UNDELETE,
|
||||
} from '@/event/Events'
|
||||
import { PublishNameType } from '@/graphql/enum/PublishNameType'
|
||||
import { isValidPassword } from '@/password/EncryptorUtils'
|
||||
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
||||
@ -67,6 +68,7 @@ import { backendLogger as logger } from '@/server/logger'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
import { delay } from '@/util/utilities'
|
||||
|
||||
import random from 'random-bigint'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
@ -79,7 +81,7 @@ import { getUserCreations } from './util/creations'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { findUsers } from './util/findUsers'
|
||||
import { getKlicktippState } from './util/getKlicktippState'
|
||||
import { Location2Point } from './util/Location2Point'
|
||||
import { Location2Point, Point2Location } from './util/Location2Point'
|
||||
import { setUserRole, deleteUserRole } from './util/modifyUserRole'
|
||||
import { sendUserToGms } from './util/sendUserToGms'
|
||||
import { syncHumhub } from './util/syncHumhub'
|
||||
@ -107,7 +109,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const activationLink = (verificationCode: string): string => {
|
||||
logger.debug(`activationLink(${verificationCode})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD + verificationCode.toString()
|
||||
}
|
||||
|
||||
const newGradidoID = async (): Promise<string> => {
|
||||
@ -148,7 +150,16 @@ export class UserResolver {
|
||||
): Promise<User> {
|
||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||
email = email.trim().toLowerCase()
|
||||
const dbUser = await findUserByEmail(email)
|
||||
let dbUser: DbUser
|
||||
|
||||
try {
|
||||
dbUser = await findUserByEmail(email)
|
||||
} catch (e) {
|
||||
// simulate delay which occur on password encryption 650 ms +- 50 rnd
|
||||
await delay(650 + Math.floor(Math.random() * 101) - 50)
|
||||
throw e
|
||||
}
|
||||
|
||||
if (dbUser.deletedAt) {
|
||||
throw new LogError('This user was permanently deleted. Contact support for questions', dbUser)
|
||||
}
|
||||
@ -160,7 +171,7 @@ export class UserResolver {
|
||||
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||
throw new LogError('The User has not set a password yet', dbUser)
|
||||
}
|
||||
if (!verifyPassword(dbUser, password)) {
|
||||
if (!(await verifyPassword(dbUser, password))) {
|
||||
throw new LogError('No user with this credentials', dbUser)
|
||||
}
|
||||
|
||||
@ -168,14 +179,15 @@ export class UserResolver {
|
||||
let humhubUserPromise: Promise<IRestResponse<GetUser>> | undefined
|
||||
const klicktippStatePromise = getKlicktippState(dbUser.emailContact.email)
|
||||
if (CONFIG.HUMHUB_ACTIVE && dbUser.humhubAllowed) {
|
||||
const getHumhubUser = new PostUser(dbUser)
|
||||
humhubUserPromise = HumHubClient.getInstance()?.userByUsernameAsync(
|
||||
dbUser.alias ?? dbUser.gradidoID,
|
||||
getHumhubUser.account.username,
|
||||
)
|
||||
}
|
||||
|
||||
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
dbUser.password = encryptPassword(dbUser, password)
|
||||
dbUser.password = await encryptPassword(dbUser, password)
|
||||
await dbUser.save()
|
||||
}
|
||||
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
|
||||
@ -313,6 +325,8 @@ export class UserResolver {
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.language = language
|
||||
// enable humhub from now on for new user
|
||||
dbUser.humhubAllowed = true
|
||||
if (alias && (await validateAlias(alias))) {
|
||||
dbUser.alias = alias
|
||||
}
|
||||
@ -357,10 +371,9 @@ export class UserResolver {
|
||||
throw new LogError('Error while updating dbUser', error)
|
||||
})
|
||||
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
emailContact.emailVerificationCode.toString(),
|
||||
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
||||
const activationLink = `${
|
||||
CONFIG.EMAIL_LINK_VERIFICATION
|
||||
}${emailContact.emailVerificationCode.toString()}${redeemCode ? `/${redeemCode}` : ''}`
|
||||
|
||||
void sendAccountActivationEmail({
|
||||
firstName,
|
||||
@ -383,6 +396,9 @@ export class UserResolver {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.info('createUser() successful...')
|
||||
if (CONFIG.HUMHUB_ACTIVE) {
|
||||
void syncHumhub(null, dbUser)
|
||||
}
|
||||
|
||||
if (redeemCode) {
|
||||
eventRegisterRedeem.affectedUser = dbUser
|
||||
@ -494,7 +510,7 @@ export class UserResolver {
|
||||
|
||||
// Update Password
|
||||
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
user.password = encryptPassword(user, password)
|
||||
user.password = await encryptPassword(user, password)
|
||||
logger.debug('User credentials updated ...')
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
@ -624,13 +640,13 @@ export class UserResolver {
|
||||
)
|
||||
}
|
||||
|
||||
if (!verifyPassword(user, password)) {
|
||||
if (!(await verifyPassword(user, password))) {
|
||||
throw new LogError(`Old password is invalid`)
|
||||
}
|
||||
|
||||
// Save new password hash and newly encrypted private key
|
||||
user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
|
||||
user.password = encryptPassword(user, passwordNew)
|
||||
user.password = await encryptPassword(user, passwordNew)
|
||||
}
|
||||
|
||||
// Save hideAmountGDD value
|
||||
@ -683,17 +699,25 @@ export class UserResolver {
|
||||
await EVENT_USER_INFO_UPDATE(user)
|
||||
|
||||
// validate if user settings are changed with relevance to update gms-user
|
||||
if (CONFIG.GMS_ACTIVE && updateUserInGMS) {
|
||||
logger.debug(`changed user-settings relevant for gms-user update...`)
|
||||
const homeCom = await getHomeCommunity()
|
||||
if (homeCom.gmsApiKey !== null) {
|
||||
logger.debug(`gms-user update...`, user)
|
||||
await updateGmsUser(homeCom.gmsApiKey, new GmsUser(user))
|
||||
logger.debug(`gms-user update successfully.`)
|
||||
try {
|
||||
if (CONFIG.GMS_ACTIVE && updateUserInGMS) {
|
||||
logger.debug(`changed user-settings relevant for gms-user update...`)
|
||||
const homeCom = await getHomeCommunity()
|
||||
if (homeCom.gmsApiKey !== null) {
|
||||
logger.debug(`send User to Gms...`, user)
|
||||
await sendUserToGms(user, homeCom)
|
||||
logger.debug(`sendUserToGms successfully.`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('error sync user with gms', e)
|
||||
}
|
||||
if (CONFIG.HUMHUB_ACTIVE) {
|
||||
await syncHumhub(updateUserInfosArgs, user)
|
||||
try {
|
||||
if (CONFIG.HUMHUB_ACTIVE) {
|
||||
await syncHumhub(updateUserInfosArgs, user)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('error sync user with humhub', e)
|
||||
}
|
||||
|
||||
return true
|
||||
@ -712,14 +736,35 @@ export class UserResolver {
|
||||
@Authorized([RIGHTS.GMS_USER_PLAYGROUND])
|
||||
@Query(() => GmsUserAuthenticationResult)
|
||||
async authenticateGmsUserSearch(@Ctx() context: Context): Promise<GmsUserAuthenticationResult> {
|
||||
logger.info(`authUserForGmsUserSearch()...`)
|
||||
logger.info(`authenticateGmsUserSearch()...`)
|
||||
const dbUser = getUser(context)
|
||||
let result: GmsUserAuthenticationResult
|
||||
let result = new GmsUserAuthenticationResult()
|
||||
if (context.token) {
|
||||
result = await authenticateGmsUserPlayground(context.token, dbUser)
|
||||
logger.info('authUserForGmsUserSearch=', result)
|
||||
const homeCom = await getHomeCommunity()
|
||||
if (!homeCom.gmsApiKey) {
|
||||
throw new LogError('authenticateGmsUserSearch missing HomeCommunity GmsApiKey')
|
||||
}
|
||||
result = await authenticateGmsUserPlayground(homeCom.gmsApiKey, context.token, dbUser)
|
||||
logger.info('authenticateGmsUserSearch=', result)
|
||||
} else {
|
||||
throw new LogError('authUserForGmsUserSearch without token')
|
||||
throw new LogError('authenticateGmsUserSearch missing valid user login-token')
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.GMS_USER_PLAYGROUND])
|
||||
@Query(() => UserLocationResult)
|
||||
async userLocation(@Ctx() context: Context): Promise<UserLocationResult> {
|
||||
logger.info(`userLocation()...`)
|
||||
const dbUser = getUser(context)
|
||||
const result = new UserLocationResult()
|
||||
if (context.token) {
|
||||
const homeCom = await getHomeCommunity()
|
||||
result.communityLocation = Point2Location(homeCom.location as Point)
|
||||
result.userLocation = Point2Location(dbUser.location as Point)
|
||||
logger.info('userLocation=', result)
|
||||
} else {
|
||||
throw new LogError('userLocation missing valid user login-token')
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -733,7 +778,8 @@ export class UserResolver {
|
||||
if (!humhubClient) {
|
||||
throw new LogError('cannot create humhub client')
|
||||
}
|
||||
const username = dbUser.alias ?? dbUser.gradidoID
|
||||
const userNameLogic = new PublishNameLogic(dbUser)
|
||||
const username = userNameLogic.getUsername(dbUser.humhubPublishName as PublishNameType)
|
||||
let humhubUser = await humhubClient.userByUsername(username)
|
||||
if (!humhubUser) {
|
||||
humhubUser = await humhubClient.userByEmail(dbUser.emailContact.email)
|
||||
@ -744,7 +790,7 @@ export class UserResolver {
|
||||
if (humhubUser.account.status !== 1) {
|
||||
throw new LogError('user status is not 1', humhubUser.account.status)
|
||||
}
|
||||
return await humhubClient.createAutoLoginUrl(username)
|
||||
return await humhubClient.createAutoLoginUrl(humhubUser.account.username)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEARCH_ADMIN_USERS])
|
||||
@ -954,16 +1000,15 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
const dbUserContact = await DbUserContact.findOneOrFail({
|
||||
where: { email },
|
||||
const dbUser = await DbUser.findOneOrFail({
|
||||
where: {
|
||||
emailContact: { email },
|
||||
},
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
relations: { userRoles: true, emailContact: true },
|
||||
}).catch(() => {
|
||||
throw new LogError('No user with this credentials', email)
|
||||
})
|
||||
const dbUser = dbUserContact.user
|
||||
dbUser.emailContact = dbUserContact
|
||||
dbUser.userRoles = await UserRole.find({ where: { userId: dbUser.id } })
|
||||
return dbUser
|
||||
}
|
||||
|
||||
|
||||
@ -25,6 +25,8 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'], con: Connection
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
|
||||
@ -7,13 +7,14 @@ import { backendLogger as logger } from '@/server/logger'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
export async function authenticateGmsUserPlayground(
|
||||
apiKey: string,
|
||||
token: string,
|
||||
dbUser: DbUser,
|
||||
): Promise<GmsUserAuthenticationResult> {
|
||||
const result = new GmsUserAuthenticationResult()
|
||||
const dashboardUrl = ensureUrlEndsWithSlash(CONFIG.GMS_DASHBOARD_URL)
|
||||
|
||||
result.url = dashboardUrl.concat('playground')
|
||||
result.url = dashboardUrl.concat('usersearch-playground')
|
||||
result.token = await verifyAuthToken(dbUser.communityUuid, token)
|
||||
logger.info('GmsUserAuthenticationResult:', result)
|
||||
return result
|
||||
|
||||
@ -12,6 +12,8 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
import { getOpenCreations, getUserCreation } from './creations'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let mutate: ApolloServerTestClient['mutate'], con: Connection
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
|
||||
@ -16,6 +16,8 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
import { findUserByIdentifier } from './findUserByIdentifier'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
let con: Connection
|
||||
let testEnv: {
|
||||
mutate: ApolloServerTestClient['mutate']
|
||||
|
||||
@ -32,6 +32,8 @@ import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
|
||||
|
||||
import { sendTransactionsToDltConnector } from './sendTransactionsToDltConnector'
|
||||
|
||||
jest.mock('@/password/EncryptorUtils')
|
||||
|
||||
/*
|
||||
// Mock the GraphQLClient
|
||||
jest.mock('graphql-request', () => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { createGmsUser } from '@/apis/gms/GmsClient'
|
||||
import { createGmsUser, updateGmsUser } from '@/apis/gms/GmsClient'
|
||||
import { GmsUser } from '@/apis/gms/model/GmsUser'
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
@ -14,13 +14,20 @@ export async function sendUserToGms(user: DbUser, homeCom: DbCommunity): Promise
|
||||
logger.debug('User send to GMS:', user)
|
||||
const gmsUser = new GmsUser(user)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
if (await createGmsUser(homeCom.gmsApiKey, gmsUser)) {
|
||||
logger.debug('GMS user published successfully:', gmsUser)
|
||||
user.gmsRegistered = true
|
||||
user.gmsRegisteredAt = new Date()
|
||||
await DbUser.save(user)
|
||||
logger.debug('mark user as gms published:', user)
|
||||
if (!user.gmsRegistered && user.gmsRegisteredAt === null) {
|
||||
logger.debug('create user in gms:', gmsUser)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
if (await createGmsUser(homeCom.gmsApiKey, gmsUser)) {
|
||||
logger.debug('GMS user published successfully:', gmsUser)
|
||||
await updateUserGmsStatus(user)
|
||||
}
|
||||
} else {
|
||||
logger.debug('update user in gms:', gmsUser)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
if (await updateGmsUser(homeCom.gmsApiKey, gmsUser)) {
|
||||
logger.debug('GMS user published successfully:', gmsUser)
|
||||
await updateUserGmsStatus(user)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (CONFIG.GMS_CREATE_USER_THROW_ERRORS) {
|
||||
@ -30,3 +37,11 @@ export async function sendUserToGms(user: DbUser, homeCom: DbCommunity): Promise
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateUserGmsStatus(user: DbUser) {
|
||||
logger.debug('updateUserGmsStatus:', user)
|
||||
user.gmsRegistered = true
|
||||
user.gmsRegisteredAt = new Date()
|
||||
await DbUser.save(user)
|
||||
logger.debug('mark user as gms published:', user)
|
||||
}
|
||||
|
||||
@ -119,6 +119,7 @@ export async function settlePendingSenderTransaction(
|
||||
lastName: recipient.lastName,
|
||||
email: recipient.emailContact.email,
|
||||
language: recipient.language,
|
||||
memo,
|
||||
senderFirstName: sender.firstName,
|
||||
senderLastName: sender.lastName,
|
||||
senderEmail: sender.emailContact.email,
|
||||
|
||||
@ -7,11 +7,12 @@ import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
export async function syncHumhub(
|
||||
updateUserInfosArg: UpdateUserInfosArgs,
|
||||
updateUserInfosArg: UpdateUserInfosArgs | null,
|
||||
user: User,
|
||||
): Promise<void> {
|
||||
// check for humhub relevant changes
|
||||
if (
|
||||
updateUserInfosArg &&
|
||||
updateUserInfosArg.alias === undefined &&
|
||||
updateUserInfosArg.firstName === undefined &&
|
||||
updateUserInfosArg.lastName === undefined &&
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
},
|
||||
"general": {
|
||||
"amountGDD": "Betrag: {amountGDD} GDD",
|
||||
"answerNow": "Jetzt antworten",
|
||||
"completeRegistration": "Registrierung abschließen",
|
||||
"contribution": "Gemeinwohl-Beitrag: {contributionMemo}",
|
||||
"contributionDetails": "Beitragsdetails",
|
||||
@ -63,6 +64,7 @@
|
||||
"helloName": "Hallo {firstName} {lastName},",
|
||||
"linkValidity": "Der Link hat eine Gültigkeit von {hours} Stunden.\nSollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen.",
|
||||
"linkValidityWithMinutes": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten.\nSollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen.",
|
||||
"message": "Nachricht",
|
||||
"newLink": "Neuer Link",
|
||||
"orCopyLink": "Oder kopiere den Link in dein Browserfenster.",
|
||||
"pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail.",
|
||||
@ -86,8 +88,9 @@
|
||||
"title": "{senderFirstName} {senderLastName} hat deinen Gradido-Link eingelöst"
|
||||
},
|
||||
"transactionReceived": {
|
||||
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.",
|
||||
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD erhalten von {senderFirstName} {senderLastName}",
|
||||
"subject": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet",
|
||||
"replySubject": "Re: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet",
|
||||
"title": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
|
||||
}
|
||||
},
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
},
|
||||
"general": {
|
||||
"amountGDD": "Amount: {amountGDD} GDD",
|
||||
"answerNow": "Reply",
|
||||
"completeRegistration": "Complete registration",
|
||||
"contribution": "Contribution: : {contributionMemo}",
|
||||
"contributionDetails": "Contribution details",
|
||||
@ -63,6 +64,7 @@
|
||||
"helloName": "Hello {firstName} {lastName},",
|
||||
"linkValidity": "The link has a validity of {hours} hours.\nIf the validity of the link has already expired, you can have a new link sent to you here.",
|
||||
"linkValidityWithMinutes": "The link has a validity of {hours} hours and {minutes} minutes.\nIf the validity of the link has already expired, you can have a new link sent to you here.",
|
||||
"message": "Message",
|
||||
"newLink": "New link",
|
||||
"orCopyLink": "Or copy the link into your browser window.",
|
||||
"pleaseDoNotReply": "Please do not reply to this email.",
|
||||
@ -86,7 +88,8 @@
|
||||
"title": "{senderFirstName} {senderLastName} has redeemed your Gradido link"
|
||||
},
|
||||
"transactionReceived": {
|
||||
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).",
|
||||
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName}",
|
||||
"replySubject": "RE: {senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido",
|
||||
"subject": "{senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido",
|
||||
"title": "{senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido"
|
||||
}
|
||||
|
||||
53
backend/src/password/EncryptionWorker.ts
Normal file
53
backend/src/password/EncryptionWorker.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { worker } from 'workerpool'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import {
|
||||
crypto_box_SEEDBYTES,
|
||||
crypto_hash_sha512_init,
|
||||
crypto_hash_sha512_update,
|
||||
crypto_hash_sha512_final,
|
||||
crypto_hash_sha512_BYTES,
|
||||
crypto_hash_sha512_STATEBYTES,
|
||||
crypto_shorthash_BYTES,
|
||||
crypto_pwhash_SALTBYTES,
|
||||
crypto_pwhash,
|
||||
crypto_shorthash,
|
||||
} from 'sodium-native'
|
||||
|
||||
export const SecretKeyCryptographyCreateKey = (
|
||||
salt: string,
|
||||
password: string,
|
||||
configLoginAppSecret: Buffer,
|
||||
configLoginServerKey: Buffer,
|
||||
): bigint => {
|
||||
const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES)
|
||||
crypto_hash_sha512_init(state)
|
||||
crypto_hash_sha512_update(state, Buffer.from(salt))
|
||||
crypto_hash_sha512_update(state, configLoginAppSecret)
|
||||
const hash = Buffer.alloc(crypto_hash_sha512_BYTES)
|
||||
crypto_hash_sha512_final(state, hash)
|
||||
|
||||
const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES)
|
||||
const opsLimit = 10
|
||||
const memLimit = 33554432
|
||||
const algo = 2
|
||||
crypto_pwhash(
|
||||
encryptionKey,
|
||||
Buffer.from(password),
|
||||
hash.slice(0, crypto_pwhash_SALTBYTES),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
algo,
|
||||
)
|
||||
|
||||
const encryptionKeyHash = Buffer.alloc(crypto_shorthash_BYTES)
|
||||
crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
||||
return encryptionKeyHash.readBigUInt64LE()
|
||||
}
|
||||
|
||||
if (CONFIG.USE_CRYPTO_WORKER === true && typeof process.send === 'function') {
|
||||
worker({
|
||||
SecretKeyCryptographyCreateKey,
|
||||
})
|
||||
}
|
||||
@ -2,7 +2,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { cpus } from 'os'
|
||||
import path from 'path'
|
||||
|
||||
import { User } from '@entity/User'
|
||||
import { Pool, pool } from 'workerpool'
|
||||
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
|
||||
@ -10,61 +14,73 @@ import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import {
|
||||
crypto_shorthash_KEYBYTES,
|
||||
crypto_box_SEEDBYTES,
|
||||
crypto_hash_sha512_init,
|
||||
crypto_hash_sha512_update,
|
||||
crypto_hash_sha512_final,
|
||||
crypto_hash_sha512_BYTES,
|
||||
crypto_hash_sha512_STATEBYTES,
|
||||
crypto_shorthash_BYTES,
|
||||
crypto_pwhash_SALTBYTES,
|
||||
crypto_pwhash,
|
||||
crypto_shorthash,
|
||||
} from 'sodium-native'
|
||||
import { crypto_shorthash_KEYBYTES } from 'sodium-native'
|
||||
|
||||
import { SecretKeyCryptographyCreateKey as SecretKeyCryptographyCreateKeySync } from './EncryptionWorker'
|
||||
|
||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||
|
||||
let encryptionWorkerPool: Pool | undefined
|
||||
|
||||
if (CONFIG.USE_CRYPTO_WORKER === true) {
|
||||
encryptionWorkerPool = pool(
|
||||
path.join(__dirname, '..', '..', 'build', 'src', 'password', '/EncryptionWorker.js'),
|
||||
{
|
||||
// TODO: put maxQueueSize into config
|
||||
maxQueueSize: 30 * cpus().length,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// We will reuse this for changePassword
|
||||
export const isValidPassword = (password: string): boolean => {
|
||||
return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
|
||||
}
|
||||
|
||||
export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
|
||||
logger.trace('SecretKeyCryptographyCreateKey...')
|
||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||
if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) {
|
||||
throw new LogError(
|
||||
'ServerKey has an invalid size',
|
||||
configLoginServerKey.length,
|
||||
crypto_shorthash_KEYBYTES,
|
||||
)
|
||||
/**
|
||||
* @param salt
|
||||
* @param password
|
||||
* @returns can throw an exception if worker pool is full, if more than 30 * cpu core count logins happen in a time range of 30 seconds
|
||||
*/
|
||||
export const SecretKeyCryptographyCreateKey = async (
|
||||
salt: string,
|
||||
password: string,
|
||||
): Promise<bigint> => {
|
||||
try {
|
||||
logger.trace('call worker for: SecretKeyCryptographyCreateKey')
|
||||
if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) {
|
||||
throw new LogError(
|
||||
'ServerKey has an invalid size',
|
||||
configLoginServerKey.length,
|
||||
crypto_shorthash_KEYBYTES,
|
||||
)
|
||||
}
|
||||
let result: Promise<bigint>
|
||||
if (encryptionWorkerPool) {
|
||||
result = (await encryptionWorkerPool.exec('SecretKeyCryptographyCreateKey', [
|
||||
salt,
|
||||
password,
|
||||
configLoginAppSecret,
|
||||
configLoginServerKey,
|
||||
])) as Promise<bigint>
|
||||
} else {
|
||||
result = Promise.resolve(
|
||||
SecretKeyCryptographyCreateKeySync(
|
||||
salt,
|
||||
password,
|
||||
configLoginAppSecret,
|
||||
configLoginServerKey,
|
||||
),
|
||||
)
|
||||
}
|
||||
return result
|
||||
} catch (e) {
|
||||
// pool is throwing this error
|
||||
// throw new Error('Max queue size of ' + this.maxQueueSize + ' reached');
|
||||
// will be shown in frontend to user
|
||||
throw new LogError('Server is full, please try again in 10 minutes.', e)
|
||||
}
|
||||
|
||||
const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES)
|
||||
crypto_hash_sha512_init(state)
|
||||
crypto_hash_sha512_update(state, Buffer.from(salt))
|
||||
crypto_hash_sha512_update(state, configLoginAppSecret)
|
||||
const hash = Buffer.alloc(crypto_hash_sha512_BYTES)
|
||||
crypto_hash_sha512_final(state, hash)
|
||||
|
||||
const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES)
|
||||
const opsLimit = 10
|
||||
const memLimit = 33554432
|
||||
const algo = 2
|
||||
crypto_pwhash(
|
||||
encryptionKey,
|
||||
Buffer.from(password),
|
||||
hash.slice(0, crypto_pwhash_SALTBYTES),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
algo,
|
||||
)
|
||||
|
||||
const encryptionKeyHash = Buffer.alloc(crypto_shorthash_BYTES)
|
||||
crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
||||
|
||||
return [encryptionKeyHash, encryptionKey]
|
||||
}
|
||||
|
||||
export const getUserCryptographicSalt = (dbUser: User): string => {
|
||||
|
||||
@ -3,13 +3,12 @@ import { User } from '@entity/User'
|
||||
// import { logger } from '@test/testSetup' getting error "jest is not defined"
|
||||
import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils'
|
||||
|
||||
export const encryptPassword = (dbUser: User, password: string): bigint => {
|
||||
export const encryptPassword = async (dbUser: User, password: string): Promise<bigint> => {
|
||||
const salt = getUserCryptographicSalt(dbUser)
|
||||
const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash
|
||||
const passwordHash = keyBuffer[0].readBigUInt64LE()
|
||||
return passwordHash
|
||||
return SecretKeyCryptographyCreateKey(salt, password)
|
||||
}
|
||||
|
||||
export const verifyPassword = (dbUser: User, password: string): boolean => {
|
||||
return dbUser.password.toString() === encryptPassword(dbUser, password).toString()
|
||||
export const verifyPassword = async (dbUser: User, password: string): Promise<boolean> => {
|
||||
const encryptedPassword = await encryptPassword(dbUser, password)
|
||||
return dbUser.password.toString() === encryptedPassword.toString()
|
||||
}
|
||||
|
||||
114
backend/src/password/__mocks__/EncryptorUtils.ts
Normal file
114
backend/src/password/__mocks__/EncryptorUtils.ts
Normal file
@ -0,0 +1,114 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { User } from '@entity/User'
|
||||
|
||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import {
|
||||
crypto_shorthash_KEYBYTES,
|
||||
crypto_box_SEEDBYTES,
|
||||
crypto_hash_sha512_init,
|
||||
crypto_hash_sha512_update,
|
||||
crypto_hash_sha512_final,
|
||||
crypto_hash_sha512_BYTES,
|
||||
crypto_hash_sha512_STATEBYTES,
|
||||
crypto_shorthash_BYTES,
|
||||
crypto_pwhash_SALTBYTES,
|
||||
crypto_pwhash,
|
||||
crypto_shorthash,
|
||||
crypto_pwhash_OPSLIMIT_MIN,
|
||||
crypto_pwhash_MEMLIMIT_MIN,
|
||||
} from 'sodium-native'
|
||||
|
||||
const SecretKeyCryptographyCreateKeyMock = (
|
||||
salt: string,
|
||||
password: string,
|
||||
configLoginAppSecret: Buffer,
|
||||
configLoginServerKey: Buffer,
|
||||
): bigint => {
|
||||
const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES)
|
||||
crypto_hash_sha512_init(state)
|
||||
crypto_hash_sha512_update(state, Buffer.from(salt))
|
||||
crypto_hash_sha512_update(state, configLoginAppSecret)
|
||||
const hash = Buffer.alloc(crypto_hash_sha512_BYTES)
|
||||
crypto_hash_sha512_final(state, hash)
|
||||
|
||||
const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES)
|
||||
const opsLimit = crypto_pwhash_OPSLIMIT_MIN
|
||||
const memLimit = crypto_pwhash_MEMLIMIT_MIN
|
||||
const algo = 2
|
||||
crypto_pwhash(
|
||||
encryptionKey,
|
||||
Buffer.from(password),
|
||||
hash.slice(0, crypto_pwhash_SALTBYTES),
|
||||
opsLimit,
|
||||
memLimit,
|
||||
algo,
|
||||
)
|
||||
|
||||
const encryptionKeyHash = Buffer.alloc(crypto_shorthash_BYTES)
|
||||
crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
|
||||
|
||||
return encryptionKeyHash.readBigUInt64LE()
|
||||
}
|
||||
|
||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||
|
||||
// We will reuse this for changePassword
|
||||
export const isValidPassword = (password: string): boolean => {
|
||||
return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param salt
|
||||
* @param password
|
||||
* @returns can throw an exception if worker pool is full, if more than 30 * cpu core count logins happen in a time range of 30 seconds
|
||||
*/
|
||||
export const SecretKeyCryptographyCreateKey = async (
|
||||
salt: string,
|
||||
password: string,
|
||||
): Promise<bigint> => {
|
||||
try {
|
||||
logger.trace('call worker for: SecretKeyCryptographyCreateKey')
|
||||
if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) {
|
||||
throw new LogError(
|
||||
'ServerKey has an invalid size',
|
||||
configLoginServerKey.length,
|
||||
crypto_shorthash_KEYBYTES,
|
||||
)
|
||||
}
|
||||
return Promise.resolve(
|
||||
SecretKeyCryptographyCreateKeyMock(
|
||||
salt,
|
||||
password,
|
||||
configLoginAppSecret,
|
||||
configLoginServerKey,
|
||||
),
|
||||
)
|
||||
} catch (e) {
|
||||
// pool is throwing this error
|
||||
// throw new Error('Max queue size of ' + this.maxQueueSize + ' reached');
|
||||
// will be shown in frontend to user
|
||||
throw new LogError('Server is full, please try again in 10 minutes.', e)
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserCryptographicSalt = (dbUser: User): string => {
|
||||
switch (dbUser.passwordEncryptionType) {
|
||||
case PasswordEncryptionType.NO_PASSWORD:
|
||||
throw new LogError('User has no password set', dbUser.id)
|
||||
case PasswordEncryptionType.EMAIL:
|
||||
return dbUser.emailContact.email
|
||||
case PasswordEncryptionType.GRADIDO_ID:
|
||||
return dbUser.gradidoID
|
||||
default:
|
||||
throw new LogError('Unknown password encryption type', dbUser.passwordEncryptionType)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user