Merge remote-tracking branch 'origin/master' into

2956-feature-x-com-4-introduce-public-community-info-handshake
This commit is contained in:
Claus-Peter Huebner 2023-08-17 19:52:22 +02:00
commit e75d2b89cf
364 changed files with 21621 additions and 5183 deletions

37
.github/dependabot_backend.yml vendored Normal file
View File

@ -0,0 +1,37 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/backend"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
labels:
- "devops"
- "service:backend"
- package-ecosystem: docker
directory: "/backend"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
labels:
- "devops"
- "service:docker"
- package-ecosystem: "github-actions"
directory: "/"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
labels:
- "devops"

View File

@ -36,6 +36,9 @@ backend: &backend
dht_node: &dht_node
- 'dht-node/**/*'
dlt_connector: &dlt_connector
- 'dlt-connector/**/*'
docker-compose: &docker-compose
- 'docker-compose.*'

View File

@ -29,6 +29,8 @@ jobs:
database
release
federation
dht
dlt
workflow
docker
other

View File

@ -51,7 +51,7 @@ jobs:
uses: actions/checkout@v3
- name: Lint
run: cd dht-node && yarn && yarn run lint
run: cd database && yarn && cd ../dht-node && yarn && yarn run lint
unit_test:
name: Unit Tests - DHT Node

View File

@ -0,0 +1,74 @@
name: Gradido DLT Connector Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - DLT Connector
runs-on: ubuntu-latest
outputs:
dlt_connector: ${{ steps.changes.outputs.dlt_connector }}
docker-compose: ${{ steps.changes.outputs.docker-compose }}
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:
name: Docker Build Test - DLT Connector
if: needs.files-changed.outputs.dlt_connector == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build 'test' image
run: |
docker build --target test -t "gradido/dlt-connector:test" -f dlt-connector/Dockerfile .
docker save "gradido/dlt-connector:test" > /tmp/dlt-connector.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-dlt-connector-test
path: /tmp/dlt-connector.tar
lint:
name: Lint - DLT Connector
if: needs.files-changed.outputs.dlt_connector == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Lint
run: cd dlt-connector && yarn && yarn run lint
unit_test:
name: Unit Tests - DLT Connector
if: needs.files-changed.outputs.dlt_connector == 'true' || needs.files-changed.outputs.docker-compose == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-dlt-connector-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/dlt-connector.tar
- name: Unit tests
run: docker run --env NODE_ENV=test --rm gradido/dlt-connector:test yarn run test

View File

@ -33,7 +33,6 @@ jobs:
yarn && yarn dev_reset
cd ../backend
yarn && yarn seed
cd ..
- name: Boot up test system | docker-compose frontends
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
@ -41,18 +40,35 @@ jobs:
- name: Boot up test system | docker-compose mailserver
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
- name: Sleep for 15 seconds
run: sleep 15s
- name: End-to-end tests | prepare
run: |
wget --no-verbose -O /opt/cucumber-json-formatter "https://github.com/cucumber/json-formatter/releases/download/v19.0.0/cucumber-json-formatter-linux-386"
chmod +x /opt/cucumber-json-formatter
sudo ln -fs /opt/cucumber-json-formatter /usr/bin/cucumber-json-formatter
cd e2e-tests/
yarn
- name: End-to-end tests | run tests
id: e2e-tests
run: |
cd e2e-tests/
yarn
yarn run cypress run
- name: End-to-end tests | if tests failed, upload screenshots
- name: End-to-end tests | if tests failed, compile html report
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
run: |
cd e2e-tests/
node create-cucumber-html-report.js
- name: End-to-end tests | if tests failed, get pr number
id: pr
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: 8BitJonny/gh-get-current-pr@2.2.0
- 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
with:
name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/
name: cypress-report-pr-#${{ steps.pr.outputs.number }}
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/reports/cucumber_html_report

View File

@ -4,8 +4,129 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.23.2](https://github.com/gradido/gradido/compare/1.23.1...1.23.2)
- feat(admin): contribution filtering by memo [`#3174`](https://github.com/gradido/gradido/pull/3174)
- feat(backend): after transaction creations trigger to send them to dlt-connector [`#3152`](https://github.com/gradido/gradido/pull/3152)
#### [1.23.1](https://github.com/gradido/gradido/compare/1.23.0...1.23.1)
> 10 August 2023
- chore(release): v1.23.1 [`#3173`](https://github.com/gradido/gradido/pull/3173)
- feat(dlt): add dlt-connector to release-script [`#3170`](https://github.com/gradido/gradido/pull/3170)
- fix(backend): update logfiles clearance-script [`#3168`](https://github.com/gradido/gradido/pull/3168)
- fix(backend): too much logoutput on production [`#3160`](https://github.com/gradido/gradido/pull/3160)
- perf(backend): enable logfile compression [`#3158`](https://github.com/gradido/gradido/pull/3158)
- feat(other): setup dependabot for backend package updates [`#3156`](https://github.com/gradido/gradido/pull/3156)
#### [1.23.0](https://github.com/gradido/gradido/compare/1.22.3...1.23.0)
> 25 July 2023
- chore(release): v1.23.0 [`#3157`](https://github.com/gradido/gradido/pull/3157)
- refactor(frontend): add contribution by link information to locales [`#3144`](https://github.com/gradido/gradido/pull/3144)
- fix(admin): user role in admin interface [`#3153`](https://github.com/gradido/gradido/pull/3153)
- feat(backend): 3030 feature role administration backend [`#3074`](https://github.com/gradido/gradido/pull/3074)
- feat(other): iota-tangle-connector sending transaction [`#3132`](https://github.com/gradido/gradido/pull/3132)
- feat(other): proper reporting for failing end-to-end tests [`#3096`](https://github.com/gradido/gradido/pull/3096)
- fix(other): add missing volume for dlt-connector dev docker [`#3134`](https://github.com/gradido/gradido/pull/3134)
- fix(backend): semaphore parallel redeemTransactionLink test [`#3133`](https://github.com/gradido/gradido/pull/3133)
- feat(other): iota-tangle-connector mit Hello World Message als separates Modul [`#3118`](https://github.com/gradido/gradido/pull/3118)
- refactor(database): fix database public key lengths [`#3026`](https://github.com/gradido/gradido/pull/3026)
- refactor(dht): eslint dht import [`#3044`](https://github.com/gradido/gradido/pull/3044)
#### [1.22.3](https://github.com/gradido/gradido/compare/1.22.2...1.22.3)
> 6 July 2023
- chore(release): v1.22.3 [`#3131`](https://github.com/gradido/gradido/pull/3131)
- fix(backend): corrected email-link [`#3129`](https://github.com/gradido/gradido/pull/3129)
#### [1.22.2](https://github.com/gradido/gradido/compare/1.22.1...1.22.2)
> 6 July 2023
- chore(release): v1.22.2 [`#3127`](https://github.com/gradido/gradido/pull/3127)
- fix(backend): moderation message are completely hidden from the user [`#3123`](https://github.com/gradido/gradido/pull/3123)
- fix(frontend): properly save username, do not allow to edit it again [`#3124`](https://github.com/gradido/gradido/pull/3124)
- fix(frontend): fix German "Speichern" to have capital letter [`#3122`](https://github.com/gradido/gradido/pull/3122)
#### [1.22.1](https://github.com/gradido/gradido/compare/1.22.0...1.22.1)
> 4 July 2023
- chore(release): v1.22.1 [`#3117`](https://github.com/gradido/gradido/pull/3117)
- fix(backend): use base url from config in email templates [`#3114`](https://github.com/gradido/gradido/pull/3114)
- feat(frontend): test right side layout template [`#3052`](https://github.com/gradido/gradido/pull/3052)
- feat(backend): remove iota from backend [`#3115`](https://github.com/gradido/gradido/pull/3115)
- refactor(frontend): text juni to juli [`#3116`](https://github.com/gradido/gradido/pull/3116)
- refactor(frontend): refactor changes incorporated [`#3113`](https://github.com/gradido/gradido/pull/3113)
- refactor(frontend): date from deploy changed to infotext [`#3108`](https://github.com/gradido/gradido/pull/3108)
- fix(frontend): add alias to the verifyLogin GraphQL answer. [`#3107`](https://github.com/gradido/gradido/pull/3107)
- fix(backend): moderator message don't send email to user [`#3106`](https://github.com/gradido/gradido/pull/3106)
#### [1.22.0](https://github.com/gradido/gradido/compare/1.21.0...1.22.0)
> 30 June 2023
- chore(release): v1.22.0 [`#3101`](https://github.com/gradido/gradido/pull/3101)
- fix(backend): yarn.lock after typeorm update [`#3097`](https://github.com/gradido/gradido/pull/3097)
- feat(frontend): new style for settings page [`#3040`](https://github.com/gradido/gradido/pull/3040)
- feat(admin): query users on contributions [`#3094`](https://github.com/gradido/gradido/pull/3094)
- feat(backend): user query on find contributions [`#3091`](https://github.com/gradido/gradido/pull/3091)
- fix(backend): double redeem transaction link [`#3093`](https://github.com/gradido/gradido/pull/3093)
- feat(admin): message type admin frontend [`#3073`](https://github.com/gradido/gradido/pull/3073)
- feat(other): end-to-end test scenarios for deleted and not registered user [`#3077`](https://github.com/gradido/gradido/pull/3077)
- feat(database): update typeorm [`#3078`](https://github.com/gradido/gradido/pull/3078)
- refactor(other): disable cypress test retries [`#3092`](https://github.com/gradido/gradido/pull/3092)
- test(other): update cypress [`#3056`](https://github.com/gradido/gradido/pull/3056)
- feat(other): add definition of cron-job for klicktipp export. [`#3051`](https://github.com/gradido/gradido/pull/3051)
- fix(backend): forget password not for deleted users [`#3090`](https://github.com/gradido/gradido/pull/3090)
- fix(backend): gdt server error & gdt refetch policy [`#3086`](https://github.com/gradido/gradido/pull/3086)
- feat(backend): contribution message type moderator [`#3072`](https://github.com/gradido/gradido/pull/3072)
- fix(other): linting in e2e tests directory does not work [`#3089`](https://github.com/gradido/gradido/pull/3089)
- feat(other): end-to-end test feature send coins [`#3070`](https://github.com/gradido/gradido/pull/3070)
- refactor(dht): move typescript related packages in dev Dependencies [`#3085`](https://github.com/gradido/gradido/pull/3085)
- fix(backend): jest environment [`#3084`](https://github.com/gradido/gradido/pull/3084)
- feat(frontend): transaction list page change style for small device [`#3081`](https://github.com/gradido/gradido/pull/3081)
- refactor(other): refactor state to status [`#3082`](https://github.com/gradido/gradido/pull/3082)
- feat(admin): change deny contribution btn-warning color and background-color to #e1a908 [`#3080`](https://github.com/gradido/gradido/pull/3080)
- feat(backend): bootstrap and Hello World Test [`#3041`](https://github.com/gradido/gradido/pull/3041)
- feat(frontend): change the info text on the start page [`#3049`](https://github.com/gradido/gradido/pull/3049)
- refactor(dht): eslint dht n [`#3045`](https://github.com/gradido/gradido/pull/3045)
- refactor(dht): eslint dht comments [`#3046`](https://github.com/gradido/gradido/pull/3046)
- fix(frontend): auto logout messages autohide time 5000 [`#2959`](https://github.com/gradido/gradido/pull/2959)
- feat(federation): introduce private key in community table [`#3024`](https://github.com/gradido/gradido/pull/3024)
- refactor(backend): removed klicktipp middleware and replace it with a function [`#3035`](https://github.com/gradido/gradido/pull/3035)
- feat(admin): order user search desc, fix pagination [`#3059`](https://github.com/gradido/gradido/pull/3059)
- refactor(database): eslint database eslint comments [`#3039`](https://github.com/gradido/gradido/pull/3039)
- refactor(database): eslint database import [`#3038`](https://github.com/gradido/gradido/pull/3038)
- feat(backend): apply design template to HTML e-mails [`#2938`](https://github.com/gradido/gradido/pull/2938)
- refactor(database): eslint database n [`#3037`](https://github.com/gradido/gradido/pull/3037)
- refactor(backend): sodium native with types [`#3032`](https://github.com/gradido/gradido/pull/3032)
- refactor(federation): expose private key to write home community [`#3023`](https://github.com/gradido/gradido/pull/3023)
- refactor(dht): eslint dht base configuration [`#3042`](https://github.com/gradido/gradido/pull/3042)
- refactor(federation): eslint federation base configuration [`#3047`](https://github.com/gradido/gradido/pull/3047)
- refactor(database): eslint database base configuration [`#3036`](https://github.com/gradido/gradido/pull/3036)
- feat(workflow): dht as package for lint pr workflow [`#3043`](https://github.com/gradido/gradido/pull/3043)
- refactor(federation): removed (potential) duplicate db query [`#3019`](https://github.com/gradido/gradido/pull/3019)
- refactor(federation): remove function getSeed [`#3022`](https://github.com/gradido/gradido/pull/3022)
- refactor(federation): refactor federation to use inheritance [`#2992`](https://github.com/gradido/gradido/pull/2992)
- refactor(backend): random-bigint types [`#3033`](https://github.com/gradido/gradido/pull/3033)
- fix(frontend): incorrect errormessage for wrong contribution link [`#2958`](https://github.com/gradido/gradido/pull/2958)
- refactor(federation): remove export from CommunityApi [`#3021`](https://github.com/gradido/gradido/pull/3021)
- refactor(federation): simplify newCommunityUuid [`#3020`](https://github.com/gradido/gradido/pull/3020)
- refactor(database): remove duplicate comments [`#3025`](https://github.com/gradido/gradido/pull/3025)
- feat(federation): x com 3 introduce business communities [`#2955`](https://github.com/gradido/gradido/pull/2955)
- refactor(backend): remove to do after creating issue [`#3000`](https://github.com/gradido/gradido/pull/3000)
- refactor(backend): camelcase exception for FederationClient_XX_X [`#3016`](https://github.com/gradido/gradido/pull/3016)
#### [1.21.0](https://github.com/gradido/gradido/compare/1.20.0...1.21.0)
> 19 May 2023
- chore(release): v1.21.0 [`#2998`](https://github.com/gradido/gradido/pull/2998)
- feat(frontend): preserve email after login [`#2994`](https://github.com/gradido/gradido/pull/2994)
- feat(frontend): send coins via identifier [`#2989`](https://github.com/gradido/gradido/pull/2989)
- feat(backend): export user events to klicktipp [`#2916`](https://github.com/gradido/gradido/pull/2916)

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.21.0",
"version": "1.23.2",
"license": "Apache-2.0",
"private": false,
"scripts": {

View File

@ -21,6 +21,7 @@ const mocks = {
moderator: {
id: 0,
name: 'test moderator',
roles: ['ADMIN'],
},
},
},
@ -45,7 +46,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 1,
isAdmin: null,
roles: [],
},
}
wrapper = Wrapper()
@ -61,7 +62,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 0,
isAdmin: null,
roles: ['ADMIN'],
},
}
wrapper = Wrapper()
@ -88,7 +89,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 1,
isAdmin: null,
roles: [],
},
}
wrapper = Wrapper()
@ -120,13 +121,13 @@ describe('ChangeUserRoleFormular', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
setUserRole: new Date(),
setUserRole: 'ADMIN',
},
})
propsData = {
item: {
userId: 1,
isAdmin: null,
roles: ['USER'],
},
}
wrapper = Wrapper()
@ -134,7 +135,7 @@ describe('ChangeUserRoleFormular', () => {
})
it('has selected option set to "usual user"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('user')
expect(wrapper.find('select.role-select').element.value).toBe('USER')
})
describe('change select to', () => {
@ -149,7 +150,7 @@ describe('ChangeUserRoleFormular', () => {
})
})
describe('new role', () => {
describe('new role "MODERATOR"', () => {
beforeEach(() => {
rolesToSelect.at(1).setSelected()
})
@ -181,19 +182,267 @@ describe('ChangeUserRoleFormular', () => {
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: true,
role: 'MODERATOR',
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
it('emits "updateRoles" with role moderator', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: expect.any(Date),
roles: ['MODERATOR'],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
describe('new role "ADMIN"', () => {
beforeEach(() => {
rolesToSelect.at(2).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'ADMIN',
},
}),
)
})
it('emits "updateRoles" with role moderator', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: ['ADMIN'],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})
})
describe('user has role "moderator"', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
setUserRole: null,
},
})
propsData = {
item: {
userId: 1,
roles: ['MODERATOR'],
},
}
wrapper = Wrapper()
rolesToSelect = wrapper.find('select.role-select').findAll('option')
})
it('has selected option set to "MODERATOR"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('MODERATOR')
})
describe('change select to', () => {
describe('same role', () => {
it('has "change_user_role" button disabled', () => {
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true)
})
it('does not call the API', () => {
rolesToSelect.at(1).setSelected()
expect(apolloMutateMock).not.toHaveBeenCalled()
})
})
describe('new role "USER"', () => {
beforeEach(() => {
rolesToSelect.at(0).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'USER',
},
}),
)
})
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: [],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
describe('new role "ADMIN"', () => {
beforeEach(() => {
rolesToSelect.at(2).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'ADMIN',
},
}),
)
})
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: ['ADMIN'],
},
]),
]),
@ -232,7 +481,7 @@ describe('ChangeUserRoleFormular', () => {
propsData = {
item: {
userId: 1,
isAdmin: new Date(),
roles: ['ADMIN'],
},
}
wrapper = Wrapper()
@ -240,7 +489,7 @@ describe('ChangeUserRoleFormular', () => {
})
it('has selected option set to "admin"', () => {
expect(wrapper.find('select.role-select').element.value).toBe('admin')
expect(wrapper.find('select.role-select').element.value).toBe('ADMIN')
})
describe('change select to', () => {
@ -251,11 +500,12 @@ describe('ChangeUserRoleFormular', () => {
it('does not call the API', () => {
rolesToSelect.at(1).setSelected()
// TODO: Fix this
expect(apolloMutateMock).not.toHaveBeenCalled()
})
})
describe('new role', () => {
describe('new role "USER"', () => {
beforeEach(() => {
rolesToSelect.at(0).setSelected()
})
@ -287,19 +537,90 @@ describe('ChangeUserRoleFormular', () => {
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: false,
role: 'USER',
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: null,
roles: [],
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
describe('new role "MODERATOR"', () => {
beforeEach(() => {
rolesToSelect.at(1).setSelected()
})
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
role: 'MODERATOR',
},
}),
)
})
it('emits "updateRoles"', () => {
expect(wrapper.emitted('updateRoles')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
roles: ['MODERATOR'],
},
]),
]),
@ -328,5 +649,23 @@ describe('ChangeUserRoleFormular', () => {
})
})
})
describe('authenticated user is MODERATOR', () => {
beforeEach(() => {
mocks.$store.state.moderator.roles = ['MODERATOR']
})
it('displays text with role', () => {
expect(wrapper.text()).toBe('userRole.selectRoles.admin')
})
it('has no role select', () => {
expect(wrapper.find('select.role-select').exists()).toBe(false)
})
it('has no button', () => {
expect(wrapper.find('button.btn.btn-dange').exists()).toBe(false)
})
})
})
})

View File

@ -1,7 +1,10 @@
<template>
<div class="change-user-role-formular">
<div class="shadow p-3 mb-5 bg-white rounded">
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
<div v-if="!$store.state.moderator.roles.includes('ADMIN')" class="m-3 mb-4">
{{ roles.find((role) => role.value === currentRole).text }}
</div>
<div v-else-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
{{ $t('userRole.notChangeYourSelf') }}
</div>
<div v-else class="m-3">
@ -25,8 +28,9 @@
import { setUserRole } from '../graphql/setUserRole'
const rolesValues = {
admin: 'admin',
user: 'user',
ADMIN: 'ADMIN',
MODERATOR: 'MODERATOR',
USER: 'USER',
}
export default {
@ -39,23 +43,30 @@ export default {
},
data() {
return {
currentRole: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
currentRole: this.getCurrentRole(),
roleSelected: this.getCurrentRole(),
roles: [
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
{ value: rolesValues.admin, text: this.$t('userRole.selectRoles.admin') },
{ value: rolesValues.USER, text: this.$t('userRole.selectRoles.user') },
{ value: rolesValues.MODERATOR, text: this.$t('userRole.selectRoles.moderator') },
{ value: rolesValues.ADMIN, text: this.$t('userRole.selectRoles.admin') },
],
}
},
methods: {
getCurrentRole() {
if (this.item.roles.length) return rolesValues[this.item.roles[0]]
return rolesValues.USER
},
showModal() {
this.$bvModal
.msgBoxConfirm(
this.$t('overlay.changeUserRole.question', {
username: `${this.item.firstName} ${this.item.lastName}`,
newRole:
this.roleSelected === 'admin'
this.roleSelected === rolesValues.ADMIN
? this.$t('userRole.selectRoles.admin')
: this.roleSelected === rolesValues.MODERATOR
? this.$t('userRole.selectRoles.moderator')
: this.$t('userRole.selectRoles.user'),
}),
{
@ -77,25 +88,27 @@ export default {
})
},
setUserRole(newRole, oldRole) {
const role = this.roles.find((role) => {
return role.value === newRole
})
const roleText = role.text
const roleValue = role.value
this.$apollo
.mutate({
mutation: setUserRole,
variables: {
userId: this.item.userId,
isAdmin: newRole === rolesValues.admin,
role: role.value,
},
})
.then((result) => {
this.$emit('updateIsAdmin', {
this.$emit('updateRoles', {
userId: this.item.userId,
isAdmin: result.data.setUserRole,
roles: roleValue === 'USER' ? [] : [roleValue],
})
this.toastSuccess(
this.$t('userRole.successfullyChangedTo', {
role:
result.data.setUserRole !== null
? this.$t('userRole.selectRoles.admin')
: this.$t('userRole.selectRoles.user'),
role: roleText,
}),
)
})

View File

@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesFormular from './ContributionMessagesFormular'
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
const localVue = global.localVue
@ -34,6 +35,7 @@ describe('ContributionMessagesFormular', () => {
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
jest.clearAllMocks()
})
it('has a DIV .contribution-messages-formular', () => {
@ -73,13 +75,65 @@ describe('ContributionMessagesFormular', () => {
)
})
it('emitted "update-state" with data', async () => {
expect(wrapper.emitted('update-state')).toEqual(
it('emitted "update-status" with data', async () => {
expect(wrapper.emitted('update-status')).toEqual(
expect.arrayContaining([expect.arrayContaining([42])]),
)
})
})
describe('send DIALOG contribution message with success', () => {
beforeEach(async () => {
await wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('button[data-test="submit-dialog"]').trigger('click')
})
it('moderatorMesage has `DIALOG`', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminCreateContributionMessage,
variables: {
contributionId: 42,
message: 'text form message',
messageType: 'DIALOG',
},
})
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
describe('send MODERATOR contribution message with success', () => {
beforeEach(async () => {
await wrapper.setData({
form: {
text: 'text form message',
},
})
await wrapper.find('button[data-test="submit-moderator"]').trigger('click')
})
it('moderatorMesage has `MODERATOR`', () => {
expect(apolloMutateMock).toBeCalledWith({
mutation: adminCreateContributionMessage,
variables: {
contributionId: 42,
message: 'text form message',
messageType: 'MODERATOR',
},
})
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
describe('send contribution message with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
@ -91,21 +145,5 @@ describe('ContributionMessagesFormular', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
})
})
describe('send contribution message with success', () => {
beforeEach(async () => {
wrapper.setData({
form: {
text: 'text form message',
},
})
wrapper = Wrapper()
await wrapper.find('form').trigger('submit')
})
it('toasts an success message', () => {
expect(toastSuccessSpy).toBeCalledWith('message.request')
})
})
})
})

View File

@ -1,7 +1,7 @@
<template>
<div class="contribution-messages-formular">
<div class="mt-5">
<b-form @submit.prevent="onSubmit" @reset.prevent="onReset">
<b-form @reset.prevent="onReset" @submit="onSubmit(messageType.DIALOG)">
<b-form-textarea
id="textarea"
v-model="form.text"
@ -12,8 +12,27 @@
<b-col>
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-center">
<b-button
type="button"
variant="warning"
class="text-black"
:disabled="disabled"
@click.prevent="onSubmit(messageType.MODERATOR)"
data-test="submit-moderator"
>
{{ $t('moderator.notice') }}
</b-button>
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="primary" :disabled="disabled">
<b-button
type="submit"
variant="primary"
:disabled="disabled"
@click.prevent="onSubmit(messageType.DIALOG)"
data-test="submit-dialog"
>
{{ $t('form.submit') }}
</b-button>
</b-col>
@ -39,10 +58,14 @@ export default {
text: '',
},
loading: false,
messageType: {
DIALOG: 'DIALOG',
MODERATOR: 'MODERATOR',
},
}
},
methods: {
onSubmit(event) {
onSubmit(mType) {
this.loading = true
this.$apollo
.mutate({
@ -50,11 +73,12 @@ export default {
variables: {
contributionId: this.contributionId,
message: this.form.text,
messageType: mType,
},
})
.then((result) => {
this.$emit('get-list-contribution-messages', this.contributionId)
this.$emit('update-state', this.contributionId)
this.$emit('update-status', this.contributionId)
this.form.text = ''
this.toastSuccess(this.$t('message.request'))
this.loading = false

View File

@ -1,26 +1,102 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesList from './ContributionMessagesList'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js'
import { toastErrorSpy } from '../../../test/testSetup'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue()
localVue.use(VueApollo)
const defaultData = () => {
return {
adminListContributionMessages: {
count: 4,
messages: [
{
id: 43,
message: 'A DIALOG message',
createdAt: new Date().toString(),
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 1,
isModerator: true,
},
{
id: 44,
message: 'Another DIALOG message',
createdAt: new Date().toString(),
updatedAt: null,
type: 'DIALOG',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
userId: 2,
isModerator: false,
},
{
id: 45,
message: `DATE
---
A HISTORY message
---
AMOUNT`,
createdAt: new Date().toString(),
updatedAt: null,
type: 'HISTORY',
userFirstName: 'Bibi',
userLastName: 'Bloxberg',
userId: 2,
isModerator: false,
},
{
id: 46,
message: 'A MODERATOR message',
createdAt: new Date().toString(),
updatedAt: null,
type: 'MODERATOR',
userFirstName: 'Peter',
userLastName: 'Lustig',
userId: 1,
isModerator: true,
},
],
},
}
}
describe('ContributionMessagesList', () => {
let wrapper
const adminListContributionMessagessMock = jest.fn()
mockClient.setRequestHandler(
adminListContributionMessages,
adminListContributionMessagessMock
.mockRejectedValueOnce({ message: 'Auaa!' })
.mockResolvedValue({ data: defaultData() }),
)
const propsData = {
contributionId: 42,
contributionState: 'PENDING',
contributionUserId: 108,
contributionStatus: 'PENDING',
}
const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$n: jest.fn((n) => n),
$i18n: {
locale: 'en',
},
$apollo: {
query: apolloQueryMock,
},
}
const Wrapper = () => {
@ -28,30 +104,34 @@ describe('ContributionMessagesList', () => {
localVue,
mocks,
propsData,
apolloProvider,
})
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
contributionId: propsData.contributionId,
},
}),
)
describe('server response for admin list contribution messages is error', () => {
it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Auaa!')
})
})
it('has a DIV .contribution-messages-list', () => {
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
})
describe('server response is succes', () => {
it('has a DIV .contribution-messages-list', () => {
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
})
it('has a Component ContributionMessagesFormular', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
it('has 4 messages', () => {
expect(wrapper.findAll('div.contribution-messages-list-item')).toHaveLength(4)
})
it('has a Component ContributionMessagesFormular', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
})
})
})
})

View File

@ -2,15 +2,17 @@
<div class="contribution-messages-list">
<b-container>
<div v-for="message in messages" v-bind:key="message.id">
<contribution-messages-list-item :message="message" />
<contribution-messages-list-item
:message="message"
:contributionUserId="contributionUserId"
/>
</div>
</b-container>
<div v-if="contributionState === 'PENDING' || contributionState === 'IN_PROGRESS'">
<div v-if="contributionStatus === 'PENDING' || contributionStatus === 'IN_PROGRESS'">
<contribution-messages-formular
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
@get-list-contribution-messages="$apollo.queries.Messages.refetch()"
@update-status="updateStatus"
/>
</div>
</div>
@ -18,7 +20,7 @@
<script>
import ContributionMessagesListItem from './slots/ContributionMessagesListItem'
import ContributionMessagesFormular from '../ContributionMessages/ContributionMessagesFormular'
import { listContributionMessages } from '../../graphql/listContributionMessages.js'
import { adminListContributionMessages } from '../../graphql/adminListContributionMessages.js'
export default {
name: 'ContributionMessagesList',
@ -31,39 +33,43 @@ export default {
type: Number,
required: true,
},
contributionState: {
contributionStatus: {
type: String,
required: true,
},
contributionUserId: {
type: Number,
required: true,
},
},
data() {
return {
messages: [],
}
},
methods: {
getListContributionMessages(id) {
this.$apollo
.query({
query: listContributionMessages,
variables: {
contributionId: id,
},
fetchPolicy: 'no-cache',
})
.then((result) => {
this.messages = result.data.listContributionMessages.messages
})
.catch((error) => {
this.toastError(error.message)
})
},
updateState(id) {
this.$emit('update-state', id)
apollo: {
Messages: {
query() {
return adminListContributionMessages
},
variables() {
return {
contributionId: this.contributionId,
}
},
fetchPolicy: 'no-cache',
update({ adminListContributionMessages }) {
this.messages = adminListContributionMessages.messages
},
error({ message }) {
this.toastError(message)
},
},
},
created() {
this.getListContributionMessages(this.contributionId)
methods: {
updateStatus(id) {
this.$emit('update-status', id)
},
},
}
</script>

View File

@ -13,11 +13,20 @@ describe('ContributionMessagesListItem', () => {
$t: jest.fn((t) => t),
$d: dateMock,
$n: numberMock,
$store: {
state: {
moderator: {
firstName: 'Peter',
lastName: 'Lustig',
},
},
},
}
describe('if message author has moderator role', () => {
const propsData = {
contributionId: 42,
contributionUserId: 108,
state: 'PENDING',
message: {
id: 111,
@ -51,27 +60,21 @@ describe('ContributionMessagesListItem', () => {
})
it('has the complete user name', () => {
expect(wrapper.find('div.text-right.is-moderator > span:nth-child(2)').text()).toBe(
'Peter Lustig',
)
expect(wrapper.find('[data-test="moderator-name"]').text()).toBe('Peter Lustig')
})
it('has the message creation date', () => {
expect(wrapper.find('div.text-right.is-moderator > span:nth-child(3)').text()).toMatch(
expect(wrapper.find('[data-test="moderator-date"]').text()).toMatch(
'Mon Aug 29 2022 12:23:27 GMT+0000',
)
})
it('has the moderator label', () => {
expect(wrapper.find('div.text-right.is-moderator > small:nth-child(4)').text()).toBe(
'moderator',
)
expect(wrapper.find('[data-test="moderator-label"]').text()).toBe('moderator.moderator')
})
it('has the message', () => {
expect(wrapper.find('div.text-right.is-moderator > div:nth-child(5)').text()).toBe(
'Lorem ipsum?',
)
expect(wrapper.find('[data-test="moderator-message"]').text()).toBe('Lorem ipsum?')
})
})
})
@ -79,6 +82,7 @@ describe('ContributionMessagesListItem', () => {
describe('if message author does not have moderator role', () => {
const propsData = {
contributionId: 42,
contributionUserId: 108,
state: 'PENDING',
message: {
id: 113,
@ -107,23 +111,21 @@ describe('ContributionMessagesListItem', () => {
})
it('has a DIV .text-left.is-not-moderator', () => {
expect(wrapper.find('div.text-left.is-not-moderator').exists()).toBe(true)
expect(wrapper.find('div.text-left.is-user').exists()).toBe(true)
})
it('has the complete user name', () => {
expect(wrapper.find('div.is-not-moderator.text-left > span:nth-child(2)').text()).toBe(
'Bibi Bloxberg',
)
expect(wrapper.find('[data-test="user-name"]').text()).toBe('Bibi Bloxberg')
})
it('has the message creation date', () => {
expect(wrapper.find('div.is-not-moderator.text-left > span:nth-child(3)').text()).toMatch(
expect(wrapper.find('[data-test="user-date"]').text()).toMatch(
'Mon Aug 29 2022 12:25:34 GMT+0000',
)
})
it('has the message', () => {
expect(wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)').text()).toBe(
expect(wrapper.find('[data-test="user-message"]').text()).toBe(
'Asda sdad ad asdasd, das Ass das Das.',
)
})
@ -132,6 +134,7 @@ describe('ContributionMessagesListItem', () => {
describe('links in contribtion message', () => {
const propsData = {
contributionUserId: 108,
message: {
id: 111,
message: 'Lorem ipsum?',
@ -159,7 +162,7 @@ describe('ContributionMessagesListItem', () => {
beforeEach(() => {
propsData.message.message = 'https://gradido.net/de/'
wrapper = ModeratorItemWrapper()
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
messageField = wrapper.find('[data-test="moderator-message"]')
})
it('contains the link as text', () => {
@ -176,7 +179,7 @@ describe('ContributionMessagesListItem', () => {
propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/
and here is the link to the repository: https://github.com/gradido/gradido`
wrapper = ModeratorItemWrapper()
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
messageField = wrapper.find('[data-test="moderator-message"]')
})
it('contains the whole text', () => {
@ -196,6 +199,7 @@ and here is the link to the repository: https://github.com/gradido/gradido`)
describe('contribution message type HISTORY', () => {
const propsData = {
contributionUserId: 108,
message: {
id: 111,
message: `Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time)
@ -227,7 +231,7 @@ This message also contains a link: https://gradido.net/de/
beforeEach(() => {
jest.clearAllMocks()
wrapper = itemWrapper()
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
messageField = wrapper
})
it('renders the date', () => {

View File

@ -1,17 +1,37 @@
<template>
<div class="contribution-messages-list-item">
<div v-if="message.isModerator" class="text-right is-moderator">
<div v-if="isModeratorMessage" class="text-right p-2 rounded-sm mb-3" :class="boxClass">
<small class="ml-4" data-test="moderator-label">
{{ $t('moderator.moderator') }}
</small>
<small class="ml-2" data-test="moderator-date">
{{ $d(new Date(message.createdAt), 'short') }}
</small>
<span class="ml-2 mr-2" data-test="moderator-name">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<b-avatar square variant="warning"></b-avatar>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<small class="ml-4 text-success">{{ $t('moderator') }}</small>
<parse-message v-bind="message"></parse-message>
<parse-message v-bind="message" data-test="moderator-message"></parse-message>
<small v-if="isModeratorHiddenMessage">
<hr />
{{ $t('moderator.request') }}
</small>
</div>
<div v-else class="text-left is-not-moderator">
<div v-else class="text-left p-2 rounded-sm mb-3" :class="boxClass">
<b-avatar variant="info"></b-avatar>
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
<parse-message v-bind="message"></parse-message>
<span class="ml-2 mr-2" data-test="user-name">
{{ message.userFirstName }} {{ message.userLastName }}
</span>
<small class="ml-2" data-test="user-date">
{{ $d(new Date(message.createdAt), 'short') }}
</small>
<small v-if="isHistory">
<hr />
{{ $t('moderator.history') }}
<hr />
</small>
<parse-message v-bind="message" data-test="user-message"></parse-message>
</div>
</div>
</template>
@ -28,22 +48,50 @@ export default {
type: Object,
required: true,
},
contributionUserId: {
type: Number,
required: true,
},
},
computed: {
isModeratorMessage() {
return this.contributionUserId !== this.message.userId
},
isModeratorHiddenMessage() {
return this.message.type === 'MODERATOR'
},
isHistory() {
return this.message.type === 'HISTORY'
},
boxClass() {
if (this.isModeratorHiddenMessage) return 'is-moderator is-moderator-hidden-message'
if (this.isHistory) return 'is-user is-user-history-message'
if (this.isModeratorMessage) return 'is-moderator is-moderator-message'
return 'is-user is-user-message'
},
},
}
</script>
<style>
.is-not-moderator {
clear: both;
width: 75%;
margin-top: 20px;
/* background-color: rgb(261, 204, 221); */
}
.is-moderator {
clear: both;
float: right;
width: 75%;
margin-top: 20px;
margin-bottom: 20px;
/* background-color: rgb(255, 255, 128); */
}
.is-moderator-message {
background-color: rgb(228, 237, 245);
}
.is-moderator-hidden-message {
background-color: rgb(217, 161, 228);
}
.is-user {
clear: both;
width: 75%;
}
.is-user-message {
background-color: rgb(236, 235, 213);
}
.is-user-history-message {
background-color: rgb(235, 226, 57);
}
</style>

View File

@ -28,7 +28,7 @@ const defaultData = () => {
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -39,6 +39,7 @@ const defaultData = () => {
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
moderatorId: null,
},
{
id: 2,
@ -50,7 +51,7 @@ const defaultData = () => {
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -61,6 +62,7 @@ const defaultData = () => {
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
moderatorId: null,
},
],
},

View File

@ -34,8 +34,8 @@
{{ $t('help.transactionlist.confirmed') }}
</div>
<div>
{{ $t('transactionlist.state') }} {{ $t('math.equals') }}
{{ $t('help.transactionlist.state') }}
{{ $t('transactionlist.status') }} {{ $t('math.equals') }}
{{ $t('help.transactionlist.status') }}
</div>
</b-collapse>
</div>
@ -78,8 +78,8 @@ export default {
},
},
{
key: 'state',
label: this.$t('transactionlist.state'),
key: 'status',
label: this.$t('transactionlist.status'),
},
{
key: 'amount',

View File

@ -131,13 +131,13 @@ describe('OpenCreationsTable', () => {
})
})
describe('call updateState', () => {
describe('call updateStatus', () => {
beforeEach(() => {
wrapper.vm.updateState(4)
wrapper.vm.updateStatus(4)
})
it('emits update-state', () => {
expect(wrapper.vm.$root.$emit('update-state', 4)).toBeTruthy()
it('emits update-status', () => {
expect(wrapper.vm.$root.$emit('update-status', 4)).toBeTruthy()
})
})
})

View File

@ -9,8 +9,8 @@
stacked="md"
:tbody-tr-class="rowClass"
>
<template #cell(state)="row">
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
<template #cell(status)="row">
<b-icon :icon="getStatusIcon(row.item.status)"></b-icon>
</template>
<template #cell(bookmark)="row">
<div v-if="!myself(row.item)">
@ -39,12 +39,12 @@
<b-button v-else @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
<b-icon
v-if="row.item.state === 'PENDING' && row.item.messagesCount > 0"
v-if="row.item.status === 'PENDING' && row.item.messagesCount > 0"
icon="exclamation-circle-fill"
variant="warning"
></b-icon>
<b-icon
v-if="row.item.state === 'IN_PROGRESS' && row.item.messagesCount > 0"
v-if="row.item.status === 'IN_PROGRESS' && row.item.messagesCount > 0"
icon="question-diamond"
variant="warning"
class="pl-1"
@ -102,8 +102,9 @@
<div v-else>
<contribution-messages-list
:contributionId="row.item.id"
:contributionState="row.item.state"
@update-state="updateState"
:contributionStatus="row.item.status"
:contributionUserId="row.item.userId"
@update-status="updateStatus"
/>
</div>
</template>
@ -154,15 +155,21 @@ export default {
},
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.state === 'CONFIRMED') return 'table-success'
if (item.state === 'DENIED') return 'table-warning'
if (item.state === 'DELETED') return 'table-danger'
if (item.state === 'IN_PROGRESS') return 'table-primary'
if (item.state === 'PENDING') return 'table-primary'
if (item.status === 'CONFIRMED') return 'table-success'
if (item.status === 'DENIED') return 'table-warning'
if (item.status === 'DELETED') return 'table-danger'
if (item.status === 'IN_PROGRESS') return 'table-primary'
if (item.status === 'PENDING') return 'table-primary'
},
updateState(id) {
this.$emit('update-state', id)
updateStatus(id) {
this.$emit('update-status', id)
},
},
}
</script>
<style>
.btn-warning {
background-color: #e1a908;
border-color: #e1a908;
}
</style>

View File

@ -15,6 +15,7 @@ const propsData = {
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
roles: [],
},
{
userId: 2,
@ -23,6 +24,7 @@ const propsData = {
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
roles: [],
},
{
userId: 3,
@ -31,6 +33,7 @@ const propsData = {
email: 'peter@lustig.de',
creation: [0, 0, 0],
emailChecked: true,
roles: ['ADMIN'],
},
{
userId: 4,
@ -39,6 +42,7 @@ const propsData = {
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
roles: [],
},
],
fields: [
@ -68,6 +72,7 @@ const mocks = {
moderator: {
id: 0,
name: 'test moderator',
roles: ['ADMIN'],
},
},
},
@ -96,14 +101,14 @@ describe('SearchUserTable', () => {
describe('isAdmin', () => {
beforeEach(async () => {
await wrapper.find('div.change-user-role-formular').vm.$emit('updateIsAdmin', {
await wrapper.find('div.change-user-role-formular').vm.$emit('updateRoles', {
userId: 1,
isAdmin: new Date(),
roles: ['ADMIN'],
})
})
it('emits updateIsAdmin', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual([[1, expect.any(Date)]])
expect(wrapper.emitted('updateRoles')).toEqual([[1, ['ADMIN']]])
})
})

View File

@ -79,9 +79,9 @@
<transaction-link-list v-if="!row.item.deletedAt" :userId="row.item.userId" />
</b-tab>
<b-tab :title="$t('userRole.tabTitle')">
<change-user-role-formular :item="row.item" @updateIsAdmin="updateIsAdmin" />
<change-user-role-formular :item="row.item" @updateRoles="updateRoles" />
</b-tab>
<b-tab :title="$t('delete_user')">
<b-tab v-if="$store.state.moderator.roles.includes('ADMIN')" :title="$t('delete_user')">
<deleted-user-formular :item="row.item" @updateDeletedAt="updateDeletedAt" />
</b-tab>
</b-tabs>
@ -127,8 +127,8 @@ export default {
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
},
updateIsAdmin({ userId, isAdmin }) {
this.$emit('updateIsAdmin', userId, isAdmin)
updateRoles({ userId, roles }) {
this.$emit('updateRoles', userId, roles)
},
updateDeletedAt({ userId, deletedAt }) {
this.$emit('updateDeletedAt', userId, deletedAt)

View File

@ -0,0 +1,47 @@
import { mount } from '@vue/test-utils'
import UserQuery from './UserQuery'
const localVue = global.localVue
const propsData = {
userId: 42,
}
const mocks = {
$t: jest.fn((t) => t),
}
describe('TransactionLinkList', () => {
let wrapper
const Wrapper = () => {
return mount(UserQuery, { mocks, localVue, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('has div .input-group', () => {
expect(wrapper.find('div .input-group').exists()).toBe(true)
})
it('has .test-input-criteria', () => {
expect(wrapper.find('input.test-input-criteria').exists()).toBe(true)
})
describe('set value', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('input.test-input-criteria').setValue('Test2')
})
it('emits input', () => {
expect(wrapper.emitted('input')).toBeTruthy()
})
it('emits input with value "Test2"', () => {
expect(wrapper.emitted('input')).toEqual([['Test2']])
})
})
})
})

View File

@ -0,0 +1,43 @@
<template>
<div>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="currentValue"
:placeholder="placeholderText"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="currentValue = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
</div>
</template>
<script>
export default {
name: 'UserQuery',
props: {
value: { type: String, default: '' },
placeholder: { type: String, default: '' },
},
data() {
return {
currentValue: this.value,
}
},
computed: {
placeholderText() {
return this.placeholder || this.$t('user_search')
},
},
watch: {
currentValue() {
if (this.value !== this.currentValue) {
this.$emit('input', this.currentValue)
}
},
},
}
</script>

View File

@ -1,8 +1,12 @@
import gql from 'graphql-tag'
export const adminCreateContributionMessage = gql`
mutation ($contributionId: Int!, $message: String!) {
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
mutation ($contributionId: Int!, $message: String!, $messageType: ContributionMessageType) {
adminCreateContributionMessage(
contributionId: $contributionId
message: $message
messageType: $messageType
) {
id
message
createdAt

View File

@ -1,8 +1,8 @@
import gql from 'graphql-tag'
export const listContributionMessages = gql`
export const adminListContributionMessages = gql`
query ($contributionId: Int!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
adminListContributionMessages(
contributionId: $contributionId
pageSize: $pageSize
currentPage: $currentPage

View File

@ -7,6 +7,8 @@ export const adminListContributions = gql`
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
$query: String
$noHashtag: Boolean
) {
adminListContributions(
currentPage: $currentPage
@ -14,6 +16,8 @@ export const adminListContributions = gql`
order: $order
statusFilter: $statusFilter
userId: $userId
query: $query
noHashtag: $noHashtag
) {
contributionCount
contributionList {
@ -26,7 +30,7 @@ export const adminListContributions = gql`
contributionDate
confirmedAt
confirmedBy
state
status
messagesCount
deniedAt
deniedBy

View File

@ -1,12 +1,19 @@
import gql from 'graphql-tag'
export const searchUsers = gql`
query ($searchText: String!, $currentPage: Int, $pageSize: Int, $filters: SearchUsersFilters) {
query (
$query: String!
$filters: SearchUsersFilters
$currentPage: Int = 0
$pageSize: Int = 25
$order: Order = ASC
) {
searchUsers(
searchText: $searchText
query: $query
filters: $filters
currentPage: $currentPage
pageSize: $pageSize
filters: $filters
order: $order
) {
userCount
userList {
@ -19,7 +26,7 @@ export const searchUsers = gql`
hasElopage
emailConfirmationSend
deletedAt
isAdmin
roles
}
}
}

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const setUserRole = gql`
mutation ($userId: Int!, $isAdmin: Boolean!) {
setUserRole(userId: $userId, isAdmin: $isAdmin)
mutation ($userId: Int!, $role: RoleNames!) {
setUserRole(userId: $userId, role: $role)
}
`

View File

@ -5,7 +5,7 @@ export const verifyLogin = gql`
verifyLogin {
firstName
lastName
isAdmin
roles
id
language
}

View File

@ -89,12 +89,13 @@
"submit": "Senden"
},
"GDD": "GDD",
"hashtag_symbol": "#",
"help": {
"help": "Hilfe",
"transactionlist": {
"confirmed": "Wann wurde es von einem Moderator / Admin bestätigt.",
"periods": "Für welchen Zeitraum wurde vom Mitglied eingereicht.",
"state": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
"status": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
"submitted": "Wann wurde es vom Mitglied eingereicht"
}
},
@ -108,7 +109,12 @@
"message": {
"request": "Die Anfrage wurde gesendet."
},
"moderator": "Moderator",
"moderator": {
"history": "Die Daten wurden geändert. Dies sind die alten Daten.",
"moderator": "Moderator",
"notice": "Moderator Notiz",
"request": "Diese Nachricht ist nur für die Moderatoren sichtbar!"
},
"name": "Name",
"navbar": {
"automaticContributions": "Automatische Beiträge",
@ -119,6 +125,10 @@
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
"no_filter": "Keine Filterung",
"no_filter_tooltip": "Es wird nicht nach Hashtags gefiltert",
"no_hashtag": "Ohne Hashtag",
"no_hashtag_tooltip": "Zeigt nur Schöpfungen ohne Hashtag im Kommentar an",
"open": "offen",
"open_creations": "Offene Schöpfungen",
"overlay": {
@ -184,7 +194,7 @@
"confirmed": "Bestätigt",
"memo": "Nachricht",
"period": "Zeitraum",
"state": "Status",
"status": "Status",
"submitted": "Eingereicht",
"title": "Alle geschöpften Transaktionen für den Nutzer"
},
@ -204,12 +214,14 @@
"selectLabel": "Rolle:",
"selectRoles": {
"admin": "Administrator",
"moderator": "Moderator",
"user": "einfacher Nutzer"
},
"successfullyChangedTo": "Nutzer ist jetzt „{role}“.",
"tabTitle": "Nutzer-Rolle"
},
"user_deleted": "Nutzer ist gelöscht.",
"user_memo_search": "Nutzer-Kommentar-Suche",
"user_recovered": "Nutzer ist wiederhergestellt.",
"user_search": "Nutzer-Suche"
}

View File

@ -89,12 +89,13 @@
"submit": "Send"
},
"GDD": "GDD",
"hashtag_symbol": "#",
"help": {
"help": "Help",
"transactionlist": {
"confirmed": "When was it confirmed by a moderator / admin.",
"periods": "For what period was it submitted by the member.",
"state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = rejected, CONFIRMED = confirmed]",
"status": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = rejected, CONFIRMED = confirmed]",
"submitted": "When was it submitted by the member"
}
},
@ -108,7 +109,12 @@
"message": {
"request": "Request has been sent."
},
"moderator": "Moderator",
"moderator": {
"history": "The data has been changed. This is the old data.",
"moderator": "Moderator",
"notice": "Moderator note",
"request": "This message is only visible to the moderators!"
},
"name": "Name",
"navbar": {
"automaticContributions": "Automatic Contributions",
@ -119,6 +125,10 @@
"user_search": "User search"
},
"not_open_creations": "No open creations",
"no_filter": "No Filter",
"no_filter_tooltip": "It is not filtered by hashtags",
"no_hashtag": "No Hashtag",
"no_hashtag_tooltip": "Displays only contributions without hashtag in comment",
"open": "open",
"open_creations": "Open creations",
"overlay": {
@ -184,7 +194,7 @@
"confirmed": "Confirmed",
"memo": "Message",
"period": "Period",
"state": "State",
"status": "State",
"submitted": "Submitted",
"title": "All creation-transactions for the user"
},
@ -204,12 +214,14 @@
"selectLabel": "Role:",
"selectRoles": {
"admin": "administrator",
"moderator": "moderator",
"user": "usual user"
},
"successfullyChangedTo": "User is now \"{role}\".",
"tabTitle": "User Role"
},
"user_deleted": "User is deleted.",
"user_memo_search": "User and Memo search",
"user_recovered": "User is recovered.",
"user_search": "User search"
}

View File

@ -28,7 +28,7 @@ const mocks = {
moderator: {
firstName: 'Peter',
lastName: 'Lustig',
isAdmin: '2022-08-30T07:41:31.000Z',
roles: ['ADMIN'],
id: 263,
language: 'de',
},
@ -51,7 +51,7 @@ const defaultData = () => {
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -73,7 +73,7 @@ const defaultData = () => {
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -339,8 +339,10 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['CONFIRMED'],
})
})
@ -354,8 +356,10 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
@ -370,8 +374,10 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['DENIED'],
})
})
@ -386,8 +392,10 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['DELETED'],
})
})
@ -402,8 +410,10 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
@ -422,8 +432,10 @@ describe('CreationConfirm', () => {
it('calls the API again', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 2,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
@ -437,8 +449,10 @@ describe('CreationConfirm', () => {
it('refetches contributions with proper filter and current page = 1', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
@ -449,14 +463,50 @@ describe('CreationConfirm', () => {
})
})
describe('user query', () => {
describe('with user query', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'UserQuery' }).vm.$emit('input', 'query')
})
it('calls the API with query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: 'query',
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
describe('reset query', () => {
beforeEach(() => {
wrapper.findComponent({ name: 'UserQuery' }).vm.$emit('input', '')
})
it('calls the API with empty query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
noHashtag: null,
order: 'DESC',
pageSize: 25,
query: '',
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
})
})
})
describe('update status', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-status', 2)
})
it('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
expect(wrapper.vm.items.find((obj) => obj.id === 2).status).toBe('IN_PROGRESS')
})
})

View File

@ -1,6 +1,13 @@
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
<template>
<div class="creation-confirm">
<user-query class="mb-2 mt-2" v-model="query" :placeholder="$t('user_memo_search')" />
<div class="mb-4">
<b-button class="noHashtag" variant="light" @click="swapNoHashtag" v-b-tooltip="tooltipText">
<span :style="hashtagColor">{{ $t('hashtag_symbol') }}</span>
{{ noHashtag ? $t('no_hashtag') : $t('no_filter') }}
</b-button>
</div>
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
@ -43,7 +50,7 @@
:items="items"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
@update-status="updateStatus"
@update-contributions="$apollo.queries.ListAllContributions.refetch()"
/>
@ -85,6 +92,7 @@
<script>
import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import UserQuery from '../components/UserQuery'
import { adminListContributions } from '../graphql/adminListContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
@ -103,6 +111,7 @@ export default {
components: {
OpenCreationsTable,
Overlay,
UserQuery,
},
data() {
return {
@ -114,6 +123,8 @@ export default {
rows: 0,
currentPage: 1,
pageSize: 25,
query: '',
noHashtag: null,
}
},
watch: {
@ -122,6 +133,10 @@ export default {
},
},
methods: {
swapNoHashtag() {
this.noHashtag = !!(this.noHashtag === null || this.noHashtag === false)
this.query()
},
deleteCreation() {
this.$apollo
.mutate({
@ -187,13 +202,19 @@ export default {
},
updateStatus(id) {
this.items.find((obj) => obj.id === id).messagesCount++
this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
this.items.find((obj) => obj.id === id).status = 'IN_PROGRESS'
},
formatDateOrDash(value) {
return value ? this.$d(new Date(value), 'short') : '—'
},
},
computed: {
hashtagColor() {
return this.noHashtag ? 'color: red' : 'color: black'
},
tooltipText() {
return this.noHashtag ? this.$t('no_hashtag_tooltip') : this.$t('no_filter_tooltip')
},
fields() {
return [
[
@ -217,7 +238,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'moderatorId', label: this.$t('moderator') },
{ key: 'moderatorId', label: this.$t('moderator.moderator') },
{ key: 'editCreation', label: this.$t('chat') },
{ key: 'confirm', label: this.$t('save') },
],
@ -254,7 +275,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'confirmedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
@ -290,7 +311,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'deniedBy', label: this.$t('moderator') },
{ key: 'deniedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
@ -326,12 +347,12 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'deletedBy', label: this.$t('moderator') },
{ key: 'deletedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// all contributions
{ key: 'state', label: this.$t('status') },
{ key: 'status', label: this.$t('status') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -363,7 +384,7 @@ export default {
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'confirmedBy', label: this.$t('moderator.moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
][this.tabIndex]
@ -409,6 +430,8 @@ export default {
currentPage: this.currentPage,
pageSize: this.pageSize,
statusFilter: this.statusFilter,
query: this.query,
noHashtag: this.noHashtag,
}
},
fetchPolicy: 'no-cache',

View File

@ -43,7 +43,7 @@ const defaultData = () => {
memo: 'Danke für alles',
date: new Date(),
moderatorId: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
@ -65,7 +65,7 @@ const defaultData = () => {
memo: 'Gut Ergattert',
date: new Date(),
moderatorId: 1,
state: 'PENDING',
status: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,

View File

@ -10,11 +10,22 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
userCount: 4,
userList: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
userId: 4,
firstName: 'New',
lastName: 'User',
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
roles: [],
deletedAt: null,
},
{
userId: 3,
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
roles: ['ADMIN'],
emailChecked: true,
deletedAt: null,
},
@ -24,27 +35,20 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
roles: [],
emailChecked: true,
deletedAt: new Date(),
},
{
userId: 3,
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
creation: [0, 0, 0],
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
roles: [],
emailChecked: true,
deletedAt: null,
},
{
userId: 4,
firstName: 'New',
lastName: 'User',
email: 'new@user.ch',
creation: [1000, 1000, 1000],
emailChecked: false,
deletedAt: null,
},
],
},
},
@ -79,9 +83,10 @@ describe('UserSearch', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
query: '',
currentPage: 1,
pageSize: 25,
order: 'DESC',
filters: {
byActivated: null,
byDeleted: null,
@ -100,9 +105,10 @@ describe('UserSearch', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
query: '',
currentPage: 1,
pageSize: 25,
order: 'DESC',
filters: {
byActivated: false,
byDeleted: null,
@ -122,9 +128,10 @@ describe('UserSearch', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
query: '',
currentPage: 1,
pageSize: 25,
order: 'DESC',
filters: {
byActivated: null,
byDeleted: true,
@ -144,9 +151,10 @@ describe('UserSearch', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
query: '',
currentPage: 2,
pageSize: 25,
order: 'DESC',
filters: {
byActivated: null,
byDeleted: null,
@ -166,9 +174,10 @@ describe('UserSearch', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'search string',
query: 'search string',
currentPage: 1,
pageSize: 25,
order: 'DESC',
filters: {
byActivated: null,
byDeleted: null,
@ -181,13 +190,14 @@ describe('UserSearch', () => {
describe('reset the search field', () => {
it('calls the API with empty criteria', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
await wrapper.findComponent({ name: 'UserQuery' }).vm.$emit('input', '')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
query: '',
currentPage: 1,
pageSize: 25,
order: 'DESC',
filters: {
byActivated: null,
byDeleted: null,
@ -206,10 +216,10 @@ describe('UserSearch', () => {
it('updates user role to admin', async () => {
await wrapper
.findComponent({ name: 'SearchUserTable' })
.vm.$emit('updateIsAdmin', userId, new Date())
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(
expect.any(Date),
)
.vm.$emit('updateRoles', userId, ['ADMIN'])
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).roles).toEqual([
'ADMIN',
])
})
})
@ -217,8 +227,8 @@ describe('UserSearch', () => {
it('updates user role to usual user', async () => {
await wrapper
.findComponent({ name: 'SearchUserTable' })
.vm.$emit('updateIsAdmin', userId, null)
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).isAdmin).toEqual(null)
.vm.$emit('updateRoles', userId, [])
expect(wrapper.vm.searchResult.find((obj) => obj.userId === userId).roles).toEqual([])
})
})
})

View File

@ -23,33 +23,19 @@
</b-button>
</div>
<label>{{ $t('user_search') }}</label>
<div>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="criteria"
:placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
</div>
<user-query class="mb-4 mt-2" v-model="criteria" />
<search-user-table
type="PageUserSearch"
:items="searchResult"
:fields="fields"
@updateIsAdmin="updateIsAdmin"
@updateRoles="updateRoles"
@updateDeletedAt="updateDeletedAt"
/>
<b-pagination
pills
size="lg"
v-model="currentPage"
per-page="perPage"
:per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
@ -61,12 +47,14 @@
import SearchUserTable from '../components/Tables/SearchUserTable'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
import UserQuery from '../components/UserQuery'
export default {
name: 'UserSearch',
mixins: [creationMonths],
components: {
SearchUserTable,
UserQuery,
},
data() {
return {
@ -97,10 +85,11 @@ export default {
.query({
query: searchUsers,
variables: {
searchText: this.criteria,
query: this.criteria,
filters: this.filters,
currentPage: this.currentPage,
pageSize: this.perPage,
filters: this.filters,
order: 'DESC',
},
fetchPolicy: 'no-cache',
})
@ -112,8 +101,8 @@ export default {
this.toastError(error.message)
})
},
updateIsAdmin(userId, isAdmin) {
this.searchResult.find((obj) => obj.userId === userId).isAdmin = isAdmin
updateRoles(userId, roles) {
this.searchResult.find((obj) => obj.userId === userId).roles = roles
},
updateDeletedAt(userId, deletedAt) {
this.searchResult.find((obj) => obj.userId === userId).deletedAt = deletedAt

View File

@ -13,7 +13,7 @@ const addNavigationGuards = (router, store, apollo, i18n) => {
})
.then((result) => {
const moderator = result.data.verifyLogin
if (moderator.isAdmin) {
if (moderator.roles?.length) {
i18n.locale = moderator.language
store.commit('moderator', moderator)
next({ path: '/' })
@ -35,7 +35,7 @@ const addNavigationGuards = (router, store, apollo, i18n) => {
!CONFIG.DEBUG_DISABLE_AUTH && // we did not disabled the auth module for debug purposes
(!store.state.token || // we do not have a token
!store.state.moderator || // no moderator set in store
!store.state.moderator.isAdmin) && // user is no admin
!store.state.moderator.roles.length) && // user is no admin
to.path !== '/not-found' && // we are not on `not-found`
to.path !== '/logout' // we are not on `logout`
) {

View File

@ -5,7 +5,7 @@ const storeCommitMock = jest.fn()
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
verifyLogin: {
isAdmin: true,
roles: ['ADMIN'],
language: 'de',
},
},
@ -52,7 +52,10 @@ describe('navigation guards', () => {
})
it('commits the moderator to the store', () => {
expect(storeCommitMock).toBeCalledWith('moderator', { isAdmin: true, language: 'de' })
expect(storeCommitMock).toBeCalledWith('moderator', {
roles: ['ADMIN'],
language: 'de',
})
})
it('redirects to /', () => {
@ -60,12 +63,48 @@ describe('navigation guards', () => {
})
})
describe('with valid token and not as admin', () => {
beforeEach(() => {
describe('with valid token and as moderator', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloQueryMock.mockResolvedValue({
data: {
verifyLogin: {
isAdmin: false,
roles: ['MODERATOR'],
language: 'de',
},
},
})
await navGuard({ path: '/authenticate', query: { token: 'valid-token' } }, {}, next)
})
it('commits the token to the store', () => {
expect(storeCommitMock).toBeCalledWith('token', 'valid-token')
})
it.skip('sets the locale', () => {
expect(i18nLocaleMock).toBeCalledWith('de')
})
it('commits the moderator to the store', () => {
expect(storeCommitMock).toBeCalledWith('moderator', {
roles: ['MODERATOR'],
language: 'de',
})
})
it('redirects to /', () => {
expect(next).toBeCalledWith({ path: '/' })
})
})
describe('with valid token and no roles', () => {
beforeEach(() => {
jest.clearAllMocks()
apolloQueryMock.mockResolvedValue({
data: {
verifyLogin: {
roles: [],
language: 'de',
},
},
})
@ -77,7 +116,7 @@ describe('navigation guards', () => {
})
it('does not commit the moderator to the store', () => {
expect(storeCommitMock).not.toBeCalledWith('moderator', { isAdmin: false })
expect(storeCommitMock).not.toBeCalledWith('moderator')
})
it('redirects to /not-found', async () => {
@ -128,15 +167,22 @@ describe('navigation guards', () => {
expect(next).toBeCalledWith({ path: '/not-found' })
})
it('redirects to not found with token in store and not moderator', () => {
it('redirects to not found with token in store and not admin or moderator', () => {
store.state.token = 'valid token'
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith({ path: '/not-found' })
})
it('does not redirect with token in store and as admin', () => {
store.state.token = 'valid token'
store.state.moderator = { roles: ['ADMIN'] }
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith()
})
it('does not redirect with token in store and as moderator', () => {
store.state.token = 'valid token'
store.state.moderator = { isAdmin: true }
store.state.moderator = { roles: ['MODERATOR'] }
navGuard({ path: '/' }, {}, next)
expect(next).toBeCalledWith()
})

View File

@ -21,6 +21,10 @@ 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/

View File

@ -22,6 +22,10 @@ KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD
KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
# DltConnector
DLT_CONNECTOR=$DLT_CONNECTOR
DLT_CONNECTOR_URL=$DLT_CONNECTOR_URL
# Community
COMMUNITY_NAME=$COMMUNITY_NAME
COMMUNITY_URL=$COMMUNITY_URL

View File

@ -25,10 +25,12 @@ module.exports = {
},
node: true,
},
// the parser cannot handle the split sodium import
'import/ignore': ['sodium-native'],
},
rules: {
'no-console': 'error',
camelcase: ['error', { allow: ['FederationClient_*'] }],
camelcase: ['error', { allow: ['FederationClient_*', 'crypto_*', 'randombytes_random'] }],
'no-debugger': 'error',
'prettier/prettier': [
'error',
@ -38,7 +40,7 @@ module.exports = {
],
// import
'import/export': 'error',
'import/no-deprecated': 'error',
// 'import/no-deprecated': 'error',
'import/no-empty-named-blocks': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
@ -58,7 +60,10 @@ module.exports = {
'import/no-dynamic-require': 'error',
'import/no-internal-modules': 'off',
'import/no-relative-packages': 'error',
'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
'import/no-relative-parent-imports': [
'error',
{ ignore: ['@/*', 'random-bigint', 'sodium-native'] },
],
'import/no-self-import': 'error',
'import/no-unresolved': 'error',
'import/no-useless-path-segments': 'error',
@ -192,6 +197,9 @@ module.exports = {
{
files: ['*.test.ts'],
plugins: ['jest'],
env: {
jest: true,
},
rules: {
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',

View File

@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/ban-types */
declare module 'random-bigint' {
function random(bits: number, cb?: (err: Error, num: BigInt) => void): BigInt
export = random
}

View File

@ -0,0 +1,8 @@
// eslint-disable-next-line import/no-unresolved
export * from '@/node_modules/@types/sodium-native'
declare module 'sodium-native' {
export function crypto_hash_sha512_init(state: Buffer, key?: Buffer, outlen?: Buffer): void
export function crypto_hash_sha512_update(state: Buffer, input: Buffer): void
export function crypto_hash_sha512_final(state: Buffer, out: Buffer): void
}

View File

@ -7,7 +7,7 @@ module.exports = {
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 89,
lines: 90,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],

View File

@ -10,6 +10,7 @@
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"compress": true,
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
@ -23,6 +24,7 @@
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"compress": true,
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
@ -36,6 +38,7 @@
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"compress": true,
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
@ -49,6 +52,7 @@
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"compress": true,
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
@ -62,6 +66,7 @@
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m %s"
},
"compress": true,
"keepFileExt" : true,
"fileNameSep" : "_",
"numBackups" : 30
@ -117,7 +122,6 @@
"appenders":
[
"backend",
"out",
"errors"
],
"level": "debug",
@ -128,7 +132,6 @@
"appenders":
[
"klicktipp",
"out",
"errors"
],
"level": "debug",

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.21.0",
"version": "1.23.2",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -55,6 +55,7 @@
"@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4",
"@types/sodium-native": "^2.3.5",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",

View File

@ -0,0 +1,174 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable security/detect-object-injection */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Connection } from '@dbTools/typeorm'
import { Transaction as DbTransaction } from '@entity/Transaction'
import { Decimal } from 'decimal.js-light'
import { cleanDB, testEnvironment } from '@test/helpers'
import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { DltConnectorClient } from './DltConnectorClient'
let con: Connection
let testEnv: {
con: Connection
}
// Mock the GraphQLClient
jest.mock('graphql-request', () => {
const originalModule = jest.requireActual('graphql-request')
let testCursor = 0
return {
__esModule: true,
...originalModule,
GraphQLClient: jest.fn().mockImplementation((url: string) => {
if (url === 'invalid') {
throw new Error('invalid url')
}
return {
// why not using mockResolvedValueOnce or mockReturnValueOnce?
// I have tried, but it didn't work and return every time the first value
request: jest.fn().mockImplementation(() => {
testCursor++
if (testCursor === 4) {
return Promise.resolve(
// invalid, is 33 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc516212A',
},
},
)
} else if (testCursor === 5) {
throw Error('Connection error')
} else {
return Promise.resolve(
// valid, is 32 Bytes long as binary
{
transmitTransaction: {
dltTransactionIdHex:
'723e3fab62c5d3e2f62fd72ba4e622bcd53eff35262e3f3526327fe41bc51621',
},
},
)
}
}),
}
}),
}
})
describe('undefined DltConnectorClient', () => {
it('invalid url', () => {
CONFIG.DLT_CONNECTOR_URL = 'invalid'
CONFIG.DLT_CONNECTOR = true
const result = DltConnectorClient.getInstance()
expect(result).toBeUndefined()
CONFIG.DLT_CONNECTOR_URL = 'http://dlt-connector:6010'
})
it('DLT_CONNECTOR is false', () => {
CONFIG.DLT_CONNECTOR = false
const result = DltConnectorClient.getInstance()
expect(result).toBeUndefined()
CONFIG.DLT_CONNECTOR = true
})
})
/*
describe.skip('transmitTransaction, without db connection', () => {
const transaction = new DbTransaction()
transaction.typeId = 2 // Example transaction type ID
transaction.amount = new Decimal('10.00') // Example amount
transaction.balanceDate = new Date() // Example creation date
transaction.id = 1 // Example transaction ID
it('cannot query for transaction id', async () => {
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
})
*/
describe('transmitTransaction', () => {
beforeAll(async () => {
testEnv = await testEnvironment(logger)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
await cleanDB()
await con.close()
})
const transaction = new DbTransaction()
transaction.typeId = 2 // Example transaction type ID
transaction.amount = new Decimal('10.00') // Example amount
transaction.balanceDate = new Date() // Example creation date
transaction.id = 1 // Example transaction ID
// data needed to let save succeed
transaction.memo = "I'm a dummy memo"
transaction.userId = 1
transaction.userGradidoID = 'dummy gradido id'
/*
it.skip('cannot find transaction in db', async () => {
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
*/
it('invalid transaction type', async () => {
const localTransaction = new DbTransaction()
localTransaction.typeId = 12
try {
await DltConnectorClient.getInstance()?.transmitTransaction(localTransaction)
} catch (e) {
expect(e).toMatchObject(
new LogError('invalid transaction type id: ' + localTransaction.typeId.toString()),
)
}
})
/*
it.skip('should transmit the transaction and update the dltTransactionId in the database', async () => {
await transaction.save()
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(true)
})
it.skip('invalid dltTransactionId (maximal 32 Bytes in Binary)', async () => {
await transaction.save()
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
*/
})
/*
describe.skip('try transmitTransaction but graphql request failed', () => {
it('graphql request should throw', async () => {
const transaction = new DbTransaction()
transaction.typeId = 2 // Example transaction type ID
transaction.amount = new Decimal('10.00') // Example amount
transaction.balanceDate = new Date() // Example creation date
transaction.id = 1 // Example transaction ID
const result = await DltConnectorClient.getInstance()?.transmitTransaction(transaction)
expect(result).toBe(false)
})
})
*/

View File

@ -0,0 +1,104 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
import { gql, GraphQLClient } from 'graphql-request'
import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
const sendTransaction = gql`
mutation ($input: TransactionInput!) {
sendTransaction(data: $input) {
dltTransactionIdHex
}
}
`
// from ChatGPT
function getTransactionTypeString(id: TransactionTypeId): string {
const key = Object.keys(TransactionTypeId).find(
(key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id,
)
if (key === undefined) {
throw new LogError('invalid transaction type id: ' + id.toString())
}
return key
}
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
// and ../federation/client/FederationClientFactory.ts
/**
* A Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*/
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class DltConnectorClient {
// eslint-disable-next-line no-use-before-define
private static instance: DltConnectorClient
client: GraphQLClient
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(): DltConnectorClient | undefined {
if (!CONFIG.DLT_CONNECTOR || !CONFIG.DLT_CONNECTOR_URL) {
logger.info(`dlt-connector are disabled via config...`)
return
}
if (!DltConnectorClient.instance) {
DltConnectorClient.instance = new DltConnectorClient()
}
if (!DltConnectorClient.instance.client) {
try {
DltConnectorClient.instance.client = new GraphQLClient(CONFIG.DLT_CONNECTOR_URL, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
} catch (e) {
logger.error("couldn't connect to dlt-connector: ", e)
return
}
}
return DltConnectorClient.instance
}
/**
* transmit transaction via dlt-connector to iota
* and update dltTransactionId of transaction in db with iota message id
*/
public async transmitTransaction(transaction?: DbTransaction | null): Promise<string> {
if (transaction) {
const typeString = getTransactionTypeString(transaction.typeId)
const secondsSinceEpoch = Math.round(transaction.balanceDate.getTime() / 1000)
const amountString = transaction.amount.toString()
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(sendTransaction, {
input: {
type: typeString,
amount: amountString,
createdAt: secondsSinceEpoch,
},
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return data.sendTransaction.dltTransactionIdHex
} catch (e) {
throw new LogError('Error send sending transaction to dlt-connector: ', e)
}
} else {
throw new LogError('parameter transaction not set...')
}
}
}

View File

@ -0,0 +1,3 @@
import { RIGHTS } from './RIGHTS'
export const ADMIN_RIGHTS = [RIGHTS.SET_USER_ROLE, RIGHTS.DELETE_USER, RIGHTS.UNDELETE_USER]

View File

@ -0,0 +1,19 @@
import { RIGHTS } from './RIGHTS'
export const MODERATOR_RIGHTS = [
RIGHTS.SEARCH_USERS,
RIGHTS.ADMIN_CREATE_CONTRIBUTION,
RIGHTS.ADMIN_UPDATE_CONTRIBUTION,
RIGHTS.ADMIN_DELETE_CONTRIBUTION,
RIGHTS.ADMIN_LIST_CONTRIBUTIONS,
RIGHTS.CONFIRM_CONTRIBUTION,
RIGHTS.SEND_ACTIVATION_EMAIL,
RIGHTS.LIST_TRANSACTION_LINKS_ADMIN,
RIGHTS.CREATE_CONTRIBUTION_LINK,
RIGHTS.DELETE_CONTRIBUTION_LINK,
RIGHTS.UPDATE_CONTRIBUTION_LINK,
RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.DENY_CONTRIBUTION,
RIGHTS.ADMIN_OPEN_CREATIONS,
]

View File

@ -1,8 +1,16 @@
export enum RIGHTS {
// Inalienable
LOGIN = 'LOGIN',
COMMUNITIES = 'COMMUNITIES',
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
QUERY_OPT_IN = 'QUERY_OPT_IN',
CHECK_USERNAME = 'CHECK_USERNAME',
// User
VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE',
COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID',
UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER',
@ -10,15 +18,10 @@ export enum RIGHTS {
TRANSACTION_LIST = 'TRANSACTION_LIST',
SEND_COINS = 'SEND_COINS',
LOGOUT = 'LOGOUT',
CREATE_USER = 'CREATE_USER',
SEND_RESET_PASSWORD_EMAIL = 'SEND_RESET_PASSWORD_EMAIL',
SET_PASSWORD = 'SET_PASSWORD',
QUERY_OPT_IN = 'QUERY_OPT_IN',
UPDATE_USER_INFOS = 'UPDATE_USER_INFOS',
HAS_ELOPAGE = 'HAS_ELOPAGE',
CREATE_TRANSACTION_LINK = 'CREATE_TRANSACTION_LINK',
DELETE_TRANSACTION_LINK = 'DELETE_TRANSACTION_LINK',
QUERY_TRANSACTION_LINK = 'QUERY_TRANSACTION_LINK',
REDEEM_TRANSACTION_LINK = 'REDEEM_TRANSACTION_LINK',
LIST_TRANSACTION_LINKS = 'LIST_TRANSACTION_LINKS',
GDT_BALANCE = 'GDT_BALANCE',
@ -34,12 +37,8 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
CHECK_USERNAME = 'CHECK_USERNAME',
// Admin
// Moderator
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
@ -53,4 +52,9 @@ export enum RIGHTS {
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES = 'ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES',
// Admin
SET_USER_ROLE = 'SET_USER_ROLE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
}

View File

@ -1,40 +1,24 @@
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
import { RIGHTS } from './RIGHTS'
import { Role } from './Role'
import { RoleNames } from '@/graphql/enum/RoleNames'
export const ROLE_UNAUTHORIZED = new Role('unauthorized', INALIENABLE_RIGHTS)
export const ROLE_USER = new Role('user', [
import { ADMIN_RIGHTS } from './ADMIN_RIGHTS'
import { INALIENABLE_RIGHTS } from './INALIENABLE_RIGHTS'
import { MODERATOR_RIGHTS } from './MODERATOR_RIGHTS'
import { Role } from './Role'
import { USER_RIGHTS } from './USER_RIGHTS'
export const ROLE_UNAUTHORIZED = new Role(RoleNames.UNAUTHORIZED, INALIENABLE_RIGHTS)
export const ROLE_USER = new Role(RoleNames.USER, [...INALIENABLE_RIGHTS, ...USER_RIGHTS])
export const ROLE_MODERATOR = new Role(RoleNames.MODERATOR, [
...INALIENABLE_RIGHTS,
RIGHTS.VERIFY_LOGIN,
RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID,
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST,
RIGHTS.SEND_COINS,
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
RIGHTS.SEARCH_ADMIN_USERS,
RIGHTS.LIST_CONTRIBUTION_LINKS,
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
...USER_RIGHTS,
...MODERATOR_RIGHTS,
])
export const ROLE_ADMIN = new Role(RoleNames.ADMIN, [
...INALIENABLE_RIGHTS,
...USER_RIGHTS,
...MODERATOR_RIGHTS,
...ADMIN_RIGHTS,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
// TODO from database
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN]
export const ROLES = [ROLE_UNAUTHORIZED, ROLE_USER, ROLE_MODERATOR, ROLE_ADMIN]

View File

@ -0,0 +1,32 @@
import { RIGHTS } from './RIGHTS'
export const USER_RIGHTS = [
RIGHTS.VERIFY_LOGIN,
RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID,
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST,
RIGHTS.SEND_COINS,
RIGHTS.LOGOUT,
RIGHTS.UPDATE_USER_INFOS,
RIGHTS.HAS_ELOPAGE,
RIGHTS.CREATE_TRANSACTION_LINK,
RIGHTS.DELETE_TRANSACTION_LINK,
RIGHTS.REDEEM_TRANSACTION_LINK,
RIGHTS.LIST_TRANSACTION_LINKS,
RIGHTS.GDT_BALANCE,
RIGHTS.CREATE_CONTRIBUTION,
RIGHTS.DELETE_CONTRIBUTION,
RIGHTS.LIST_CONTRIBUTIONS,
RIGHTS.LIST_ALL_CONTRIBUTIONS,
RIGHTS.UPDATE_CONTRIBUTION,
RIGHTS.SEARCH_ADMIN_USERS,
RIGHTS.LIST_CONTRIBUTION_LINKS,
RIGHTS.COMMUNITY_STATISTICS,
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
]

View File

@ -12,14 +12,14 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0066-x-community-sendcoins-transactions_table',
DB_VERSION: '0070-add_dlt_transactions_table',
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: 'v15.2023-02-07',
EXPECTED: 'v18.2023-07-10',
CURRENT: '',
},
}
@ -51,6 +51,11 @@ const klicktipp = {
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
}
const dltConnector = {
DLT_CONNECTOR: process.env.DLT_CONNECTOR === 'true' || false,
DLT_CONNECTOR_URL: process.env.DLT_CONNECTOR_URL ?? 'http://localhost:6010',
}
const community = {
COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
@ -126,6 +131,7 @@ export const CONFIG = {
...server,
...database,
...klicktipp,
...dltConnector,
...community,
...email,
...loginServer,

File diff suppressed because it is too large Load Diff

View File

@ -94,11 +94,11 @@ describe('sendEmailTranslated', () => {
originalMessage: expect.objectContaining({
to: 'receiver@mail.org',
cc: 'support@gradido.net',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Try To Register Again With Your Email',
html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Try To Register Again With Your Email',
html: expect.stringContaining('Try To Register Again With Your Email'),
text: expect.stringContaining('TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
}),
})
})
@ -142,11 +142,11 @@ describe('sendEmailTranslated', () => {
originalMessage: expect.objectContaining({
to: CONFIG.EMAIL_TEST_RECEIVER,
cc: 'support@gradido.net',
from: `Gradido (do not answer) <${CONFIG.EMAIL_SENDER}>`,
attachments: [],
subject: 'Gradido: Try To Register Again With Your Email',
html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
from: `Gradido (emails.general.doNotAnswer) <${CONFIG.EMAIL_SENDER}>`,
attachments: expect.any(Array),
subject: 'Try To Register Again With Your Email',
html: expect.stringContaining('Try To Register Again With Your Email'),
text: expect.stringContaining('TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
}),
})
})

View File

@ -70,7 +70,36 @@ export const sendEmailTranslated = async ({
const resultSend = await email
.send({
template: path.join(__dirname, 'templates', template),
message: receiver,
message: {
...receiver,
attachments: [
{
filename: 'gradido-header.jpeg',
path: path.join(__dirname, 'templates/includes/gradido-header.jpeg'),
cid: 'gradidoheader',
},
{
filename: 'facebook-icon.png',
path: path.join(__dirname, 'templates/includes/facebook-icon.png'),
cid: 'facebookicon',
},
{
filename: 'telegram-icon.png',
path: path.join(__dirname, 'templates/includes/telegram-icon.png'),
cid: 'telegramicon',
},
{
filename: 'twitter-icon.png',
path: path.join(__dirname, 'templates/includes/twitter-icon.png'),
cid: 'twittericon',
},
{
filename: 'youtube-icon.png',
path: path.join(__dirname, 'templates/includes/youtube-icon.png'),
cid: 'youtubeicon',
},
],
},
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) => {

View File

@ -34,11 +34,9 @@ let testEnv: {
beforeAll(async () => {
testEnv = await testEnvironment(logger, localization)
con = testEnv.con
// await cleanDB()
})
afterAll(async () => {
// await cleanDB()
await con.close()
})
@ -87,8 +85,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -97,37 +97,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Message about your common good contribution',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Message about your common good contribution',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: MESSAGE ABOUT YOUR COMMON GOOD CONTRIBUTION'),
text: expect.stringContaining('MESSAGE ABOUT YOUR COMMON GOOD CONTRIBUTION'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Message about your common good contribution</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Message about your common good contribution</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.',
)
expect(result.originalMessage.html).toContain(
'To view and reply to the message, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -163,8 +143,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -173,41 +155,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Email Verification',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Email Verification',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'),
text: expect.stringContaining('EMAIL VERIFICATION'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain('<title>Gradido: Email Verification</title>')
expect(result.originalMessage.html).toContain('>Gradido: Email Verification</h1>')
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your email address has just been registered with Gradido.',
)
expect(result.originalMessage.html).toContain(
'Please click on this link to complete the registration and activate your Gradido account:',
)
expect(result.originalMessage.html).toContain(
'<a href="http://localhost/checkEmail/6627633878930542284">http://localhost/checkEmail/6627633878930542284</a>',
)
expect(result.originalMessage.html).toContain(
'or copy the link above into your browser window.',
)
expect(result.originalMessage.html).toContain(
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here:',
)
expect(result.originalMessage.html).toContain(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
)
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -240,54 +198,28 @@ describe('sendEmailVariants', () => {
})
})
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 (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Try To Register Again With Your Email',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
}),
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>',
attachments: expect.any(Array),
subject: 'Try To Register Again With Your Email',
html: expect.any(String),
text: expect.stringContaining('TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
}),
})
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Try To Register Again With Your Email</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Try To Register Again With Your Email</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your email address has just been used again to register an account with Gradido.',
)
expect(result.originalMessage.html).toContain(
'However, an account already exists for your email address.',
)
expect(result.originalMessage.html).toContain(
'Please click on the following link if you have forgotten your password:',
)
expect(result.originalMessage.html).toContain(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
)
expect(result.originalMessage.html).toContain(
'or copy the link above into your browser window.',
)
expect(result.originalMessage.html).toContain(
'If you are not the one who tried to register again, please contact our support:<br><a href="mailto:support@supportmail.com">support@supportmail.com</a>',
)
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
})
})
@ -327,8 +259,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -337,37 +271,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Your contribution to the common good was confirmed',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Your contribution to the common good was confirmed',
html: expect.any(String),
text: expect.stringContaining(
'GRADIDO: YOUR CONTRIBUTION TO THE COMMON GOOD WAS CONFIRMED',
),
text: expect.stringContaining('YOUR CONTRIBUTION TO THE COMMON GOOD WAS CONFIRMED'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your contribution to the common good was confirmed</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your contribution to the common good was confirmed</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.',
)
expect(result.originalMessage.html).toContain('Amount: 23.54 GDD')
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -405,7 +319,9 @@ describe('sendEmailVariants', () => {
},
})
})
})
describe('result', () => {
it('has expected result', () => {
expect(result).toMatchObject({
envelope: {
@ -415,37 +331,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Your common good contribution was rejected',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Your common good contribution was rejected',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS REJECTED'),
text: expect.stringContaining('YOUR COMMON GOOD CONTRIBUTION WAS REJECTED'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was rejected</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was rejected</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your public good contribution “My contribution.” was rejected by Bibi Bloxberg.',
)
expect(result.originalMessage.html).toContain(
'To see your common good contributions and related messages, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -483,8 +379,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -493,37 +391,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Your common good contribution was deleted',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Your common good contribution was deleted',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS DELETED'),
text: expect.stringContaining('YOUR COMMON GOOD CONTRIBUTION WAS DELETED'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was deleted</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was deleted</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your public good contribution “My contribution.” was deleted by Bibi Bloxberg.',
)
expect(result.originalMessage.html).toContain(
'To see your common good contributions and related messages, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -559,8 +437,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -569,39 +449,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Reset password',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Reset password',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: RESET PASSWORD'),
text: expect.stringContaining('RESET PASSWORD'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain('<title>Gradido: Reset password</title>')
expect(result.originalMessage.html).toContain('>Gradido: Reset password</h1>')
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'You, or someone else, requested a password reset for this account.',
)
expect(result.originalMessage.html).toContain('If it was you, please click on the link:')
expect(result.originalMessage.html).toContain(
'<a href="http://localhost/reset-password/3762660021544901417">http://localhost/reset-password/3762660021544901417</a>',
)
expect(result.originalMessage.html).toContain(
'or copy the link above into your browser window.',
)
expect(result.originalMessage.html).toContain(
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here:',
)
expect(result.originalMessage.html).toContain(
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
)
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -643,8 +501,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -653,36 +513,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Bibi Bloxberg has redeemed your Gradido link',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Bibi Bloxberg has redeemed your Gradido link',
html: expect.any(String),
text: expect.stringContaining('BIBI BLOXBERG HAS REDEEMED YOUR GRADIDO LINK'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Bibi Bloxberg has redeemed your Gradido link</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Bibi Bloxberg has redeemed your Gradido link</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.',
)
expect(result.originalMessage.html).toContain('Amount: 17.65 GDD')
expect(result.originalMessage.html).toContain('Message: You deserve it! 🙏🏼')
expect(result.originalMessage.html).toContain(
`You can find transaction details in your Gradido account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})
@ -722,8 +563,10 @@ describe('sendEmailVariants', () => {
},
})
})
})
it('has expected result', () => {
describe('result', () => {
it('is the expected object', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
@ -732,34 +575,17 @@ describe('sendEmailVariants', () => {
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Bibi Bloxberg has sent you 37.40 Gradido',
from: 'Gradido (emails.general.doNotAnswer) <info@gradido.net>',
attachments: expect.any(Array),
subject: 'Bibi Bloxberg has sent you 37.40 Gradido',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: BIBI BLOXBERG HAS SENT YOU 37.40 GRADIDO'),
text: expect.stringContaining('BIBI BLOXBERG HAS SENT YOU 37.40 GRADIDO'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Bibi Bloxberg has sent you 37.40 Gradido</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Bibi Bloxberg has sent you 37.40 Gradido</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).',
)
expect(result.originalMessage.html).toContain(
`You can find transaction details in your Gradido account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
it('has the correct html as snapshot', () => {
expect(result.originalMessage.html).toMatchSnapshot()
})
})
})

View File

@ -1,20 +1,16 @@
doctype html
html(lang=locale)
head
title= t('emails.accountActivation.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject')
#container.col
include ../hello.pug
p= t('emails.accountActivation.emailRegistered')
p
= t('emails.accountActivation.pleaseClickLink')
br
a(href=activationLink) #{activationLink}
br
= t('emails.general.orCopyLink')
p
= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
br
a(href=resendLink) #{resendLink}
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.accountActivation.title')
.text-block
include ../includes/salutation.pug
p= t('emails.accountActivation.emailRegistered')
.content
h2= t('emails.general.completeRegistration')
div(class="p_content")= t('emails.accountActivation.pleaseClickLink')
a.button-3(href=activationLink) #{t('emails.accountActivation.activateAccount')}
div(class="p_content")= t('emails.general.orCopyLink')
a.clink(href=activationLink) #{activationLink}
include ../includes/requestNewLink.pug

View File

@ -1,23 +1,22 @@
doctype html
html(lang=locale)
head
title= t('emails.accountMultiRegistration.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
#container.col
include ../hello.pug
p
= t('emails.accountMultiRegistration.emailReused')
br
= t('emails.accountMultiRegistration.emailExists')
p
= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
br
a(href=resendLink) #{resendLink}
br
= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
p
= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
br
a(href='mailto:' + supportEmail)= supportEmail
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.accountMultiRegistration.title')
.text-block
include ../includes/salutation.pug
p
= t('emails.accountMultiRegistration.emailReused')
br
= t('emails.accountMultiRegistration.emailExists')
.content
h2= t('emails.resetPassword.title')
div(class="p_content")= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
a.button-3(href=resendLink) #{t('emails.general.reset')}
div(class="p_content")= t('emails.general.orCopyLink')
a.clink(href=resendLink) #{resendLink}
h2(style="color: red")= t('emails.accountMultiRegistration.contactSupport')
div(class="p_content")= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
a.clink(href='mailto:' + supportEmail)= supportEmail

View File

@ -1,16 +1,14 @@
doctype html
html(lang=locale)
head
title= t('emails.addedContributionMessage.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject')
#container.col
include ../hello.pug
p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.addedContributionMessage.toSeeAndAnswerMessage')
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.addedContributionMessage.title')
.text-block
include ../includes/salutation.pug
p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo })
.content
h2= t('emails.addedContributionMessage.readMessage')
div(class="p_content")= t('emails.addedContributionMessage.toSeeAndAnswerMessage')
a.button-3(href=`${communityURL}community/contributions`) #{t('emails.general.toAccount')}
include ../includes/doNotReply.pug

View File

@ -1,16 +1,10 @@
doctype html
html(lang=locale)
head
title= t('emails.contributionConfirmed.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject')
#container.col
include ../hello.pug
p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.general.amountGDD', { amountGDD: contributionAmount })
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.contributionConfirmed.title')
.text-block
include ../includes/salutation.pug
p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { contributionMemo, senderFirstName, senderLastName, amountGDD: contributionAmount })
.content
include ../includes/contributionDetailsCTA.pug
include ../includes/doNotReply.pug

View File

@ -1,16 +1,10 @@
doctype html
html(lang=locale)
head
title= t('emails.contributionDeleted.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.contributionDeleted.subject')
#container.col
include ../hello.pug
p= t('emails.contributionDeleted.commonGoodContributionDeleted', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.contributionDeleted.toSeeContributionsAndMessages')
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.contributionDeleted.title')
.text-block
include ../includes/salutation.pug
p= t('emails.contributionDeleted.commonGoodContributionDeleted', { contributionMemo, senderFirstName, senderLastName })
.content
include ../includes/contributionDetailsCTA.pug
include ../includes/doNotReply.pug

View File

@ -1,16 +1,10 @@
doctype html
html(lang=locale)
head
title= t('emails.contributionDenied.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.contributionDenied.subject')
#container.col
include ../hello.pug
p= t('emails.contributionDenied.commonGoodContributionDenied', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.contributionDenied.toSeeContributionsAndMessages')
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.contributionDenied.title')
.text-block
include ../includes/salutation.pug
p= t('emails.contributionDenied.commonGoodContributionDenied', { contributionMemo, senderFirstName, senderLastName })
.content
include ../includes/contributionDetailsCTA.pug
include ../includes/doNotReply.pug

View File

@ -1,16 +0,0 @@
p(style='margin-top: 24px;')
= t('emails.general.sincerelyYours')
br
= t('emails.general.yourGradidoTeam')
p(style='margin-top: 24px;')= '—————'
p(style='margin-top: 24px;')
if t('general.imprintImageURL').length > 0
div(style='position: relative; left: -22px;')
img(src=t('general.imprintImageURL'), width='200', alt=t('general.imprintImageAlt'))
br
each line in t('general.imprint').split(/\n/)
= line
br
a(href='mailto:' + supportEmail)= supportEmail
br
a(href=communityURL)= communityURL

View File

@ -0,0 +1,7 @@
//-
h2= t('emails.general.contributionDetails')
div(class="p_content")= t('emails.contribution.toSeeContributionsAndMessages')
a.button-3(href=`${communityURL}community/contributions`) #{t('emails.general.toAccount')}
div(class="p_content")= t('emails.general.orCopyLink')
a.clink(href=`${communityURL}community/contributions`) #{`${communityURL}community/contributions`}

View File

@ -0,0 +1 @@
div(class="p_content")= t('emails.general.pleaseDoNotReply')

View File

@ -0,0 +1,216 @@
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 100;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 200;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 200;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 200;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 700;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 800;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 800;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 800;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* vietnamese */
/* @font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_c6Dpp_k.woff2) format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
} */
/* latin-ext */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_cqDpp_k.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 900;
src: url(https://fonts.gstatic.com/s/worksans/v18/QGYsz_wNahGAdqQ43Rh_fKDp.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,73 @@
footer
.w-container(class="footer_01")
.socialmedia
a.slink(
target="_blank"
href="https://www.facebook.com/groups/Gradido/"
)
img.bi-facebook(
alt="facebook"
loading="lazy"
src="cid:facebookicon"
)
a.slink(
target="_blank"
href="https://t.me/GradidoGruppe"
)
img.bi-telegram(
alt="Telegram"
loading="lazy"
src="cid:telegramicon"
)
a.slink(
target="_blank"
href="https://twitter.com/gradido"
)
img.bi-twitter(
alt="Twitter"
loading="lazy"
src="cid:twittericon"
)
a.slink(
target="_blank"
href="https://www.youtube.com/c/GradidoNet"
)
img.bi-youtube(
alt="youtube"
loading="lazy"
src="cid:youtubeicon"
)
.line
.footer
div(class="footer_p1")= t("emails.footer.contactOurSupport")
div(class="footer_p2")= t("emails.footer.supportEmail")
img.image(
alt="Gradido Logo"
src="https://gdd.gradido.net/img/brand/green.png"
)
div
a(
class="terms_of_use"
href="https://gradido.net/de/impressum/"
target="_blank"
)= t("emails.footer.imprint")
br
a(
class="terms_of_use"
href="https://gradido.net/de/datenschutz/"
target="_blank"
)= t("emails.footer.privacyPolicy")
div(class="footer_p1")
| Gradido-Akademie
br
| Institut für Wirtschaftsbionik
br
| Pfarrweg 2
br
| 74653 Künzelsau
br
| Deutschland
br
br
br

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,6 @@
//- This sets the greeting at the end of every e-mail
.text-block
p
= t('emails.general.sincerelyYours')
br
= t('emails.general.yourGradidoTeam')

View File

@ -0,0 +1,13 @@
header
.head
//- TODO
//- when https://gdd.gradido.net/img/gradido-email-header.jpg is on production,
//- replace this URL by https://gdd.gradido.net/img/brand/gradido-email-header.png
img.head-logo(
alt="Gradido Logo"
loading="lazy"
src="cid:gradidoheader"
)

View File

@ -0,0 +1,10 @@
//-
requestNewLink
h2= t('emails.general.requestNewLink')
if timeDurationObject.minutes == 0
div(class="p_content")= t('emails.general.linkValidity', { hours: timeDurationObject.hours })
else
div(class="p_content")= t('emails.general.linkValidityWithMinutes', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
a.button-4(href=resendLink) #{t('emails.general.newLink')}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,136 @@
body{
display: block;
font-family: "Work Sans", sans-serif;
font-size: 17px;
text-align: center;
text-align: -webkit-center;
justify-content: center;
padding: 0px;
margin: 0px;
}
h2 {
margin-top: 15px;
color: #383838;
}
.container {
max-width: 680px;
margin: 0 auto;
display: block;
}
.head-logo {
width: 100%;
height: auto;
}
.text-block {
margin-top: 20px;
color: #9ca0a8;
}
.content {
display: block;
width: 78%;
margin: 40px 1% 40px 1%;
padding: 20px 10% 40px 10%;
border-radius: 24px;
background-image: linear-gradient(180deg, #f5f5f5, #f5f5f5);
}
.p_content{
margin: 15px 0 15px 0;
line-height: 26px;
color: #9ca0a8;
}
.clink {
line-break: anywhere;
margin-bottom: 40px;
}
.button-3,
.button-4 {
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%;
}
.button-4 {
background-image: radial-gradient(circle farthest-corner at 0% 0%, #616161, #c2c2c2);
}
.socialmedia {
display: flex;
margin-top: 40px;
max-width: 600px;
}
.slink{
width: 150px;
}
.footer {
padding-bottom: 20px;
}
.footer_p1 {
margin-top: 30px;
color: #9ca0a8;
margin-bottom: 30px;
}
.footer_p2 {
color: #383838;
font-weight: bold;
}
.image {
width: 200px;
margin-top: 30px;
margin-bottom: 30px;
}
.div-block {
display: table;
margin-top: 20px;
margin-bottom: 40px;
flex-direction: row;
justify-content: center;
align-items: center;
}
.terms_of_use {
color: #9ca0a8;
}
.text-block-3 {
color: #9ca0a8;
margin-bottom: 30px;
}
.line_image,
.line {
width: 100%;
height: 13px;
margin-top: 40px;
}
.line_image {
background-image: linear-gradient(90deg, #c58d38, #c58d38 0%, #f3cd7c 35%, #dbb056 54%, #eec05f 63%, #cc9d3d);
}
.line {
background-image: linear-gradient(90deg, #c58d38, #f3cd7c 40%, #dbb056 55%, #eec05f 71%, #cc9d3d);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,26 @@
doctype html
html(lang=locale)
head
meta(
content="multipart/html; charset=UTF-8"
http-equiv="content-type"
)
meta(
name="viewport"
content="width=device-width, initial-scale=1"
)
style.
.wf-force-outline-none[tabindex="-1"]:focus{outline:none;}
style
include includes/email.css
include includes/webflow.css
body
div.container
include includes/header.pug
.wrapper
block content
include includes/greeting.pug
include includes/footer.pug

View File

@ -1,20 +1,16 @@
doctype html
html(lang=locale)
head
title= t('emails.resetPassword.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject')
#container.col
include ../hello.pug
p= t('emails.resetPassword.youOrSomeoneResetPassword')
p
= t('emails.resetPassword.pleaseClickLink')
br
a(href=resetLink) #{resetLink}
br
= t('emails.general.orCopyLink')
p
= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
br
a(href=resendLink) #{resendLink}
include ../greatingFormularImprint.pug
extends ../layout.pug
block content
h2= t('emails.resetPassword.title')
.text-block
include ../includes/salutation.pug
p= t('emails.resetPassword.youOrSomeoneResetPassword')
.content
h2= t('emails.resetPassword.title')
div(class="p_content")= t('emails.resetPassword.pleaseClickLink')
a.button-3(href=resetLink) #{t('emails.general.reset')}
div(class="p_content")= t('emails.general.orCopyLink')
a.clink(href=resetLink) #{resetLink}
include ../includes/requestNewLink.pug

View File

@ -1,19 +1,18 @@
doctype html
html(lang=locale)
head
title= t('emails.transactionLinkRedeemed.subject', { senderFirstName, senderLastName })
body
h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject', { senderFirstName, senderLastName })
#container.col
include ../hello.pug
p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail })
p
= t('emails.general.amountGDD', { amountGDD: transactionAmount })
br
= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
p
= t('emails.general.detailsYouFindOnLinkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.transactionLinkRedeemed.title', { senderFirstName, senderLastName })
.text-block
include ../includes/salutation.pug
p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail })
.content
h2= t('emails.general.transactionDetails')
div(class="p_content")= t('emails.general.amountGDD', { amountGDD: transactionAmount })
br
= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
br
= t('emails.general.detailsYouFindOnLinkToYourAccount')
a.button-3(href=`${communityURL}transactions`) #{t('emails.general.toAccount')}
include ../includes/doNotReply.pug

View File

@ -1,15 +1,15 @@
doctype html
html(lang=locale)
head
title= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
body
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
#container.col
include ../hello.pug
p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail })
p
= t('emails.general.detailsYouFindOnLinkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug
extend ../layout.pug
block content
h2= t('emails.transactionReceived.title', { senderFirstName, senderLastName, transactionAmount })
.text-block
include ../includes/salutation.pug
p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail })
.content
h2= t('emails.general.transactionDetails')
div(class="p_content")= t('emails.general.detailsYouFindOnLinkToYourAccount')
a.button-3(href=`${communityURL}transactions`) #{t('emails.general.toAccount')}
include ../includes/doNotReply.pug

View File

@ -28,7 +28,7 @@ export class FederationClient {
}
getPublicKey = async (): Promise<string | undefined> => {
logger.info('Federation: getPublicKey from endpoint', this.endpoint)
logger.debug('Federation: getPublicKey from endpoint', this.endpoint)
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = await this.client.rawRequest(getPublicKey, {})
@ -37,7 +37,7 @@ export class FederationClient {
logger.warn('Federation: getPublicKey without response data from endpoint', this.endpoint)
return
}
logger.info(
logger.debug(
'Federation: getPublicKey successful from endpoint',
this.endpoint,
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access

View File

@ -64,7 +64,8 @@ describe('validate Communities', () => {
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(GraphQLClient.prototype, 'rawRequest').mockImplementation(async () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return { data: {} } as Response<unknown> })
return { data: {} } as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
@ -168,7 +169,7 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
expect(logger.debug).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
@ -177,7 +178,7 @@ describe('validate Communities', () => {
expect(logger.warn).toBeCalledWith(
'Federation: received not matching publicKey:',
'somePubKey',
expect.stringMatching('1111111111111111111111111111111111111111111111111111111111111111'),
expect.stringMatching('11111111111111111111111111111111'),
)
})
})
@ -189,15 +190,13 @@ describe('validate Communities', () => {
return {
data: {
getPublicKey: {
publicKey: '1111111111111111111111111111111111111111111111111111111111111111',
publicKey: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables1 = {
publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
@ -221,15 +220,15 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
expect(logger.debug).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs community pubKey verified', () => {
expect(logger.info).toHaveBeenNthCalledWith(
3,
'Federation: verified community with:',
expect(logger.debug).toHaveBeenNthCalledWith(
6,
'Federation: verified community with',
'http//localhost:5001/api/',
)
})
@ -243,15 +242,13 @@ describe('validate Communities', () => {
return {
data: {
getPublicKey: {
publicKey: '1111111111111111111111111111111111111111111111111111111111111111',
publicKey: '11111111111111111111111111111111',
},
},
} as Response<unknown>
})
const variables2 = {
publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
@ -275,13 +272,13 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
expect(logger.debug).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
expect(logger.debug).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
)
@ -291,9 +288,7 @@ describe('validate Communities', () => {
let dbCom: DbFederatedCommunity
beforeEach(async () => {
const variables3 = {
publicKey: Buffer.from(
'1111111111111111111111111111111111111111111111111111111111111111',
),
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
@ -319,13 +314,13 @@ describe('validate Communities', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
expect(logger.debug).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_0/',
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
expect(logger.debug).toBeCalledWith(
'Federation: getPublicKey from endpoint',
'http//localhost:5001/api/1_1/',
)

View File

@ -12,10 +12,13 @@ import { backendLogger as logger } from '@/server/logger'
import { ApiVersionType } from './enum/apiVersionType'
export function startValidateCommunities(timerInterval: number): void {
export async function startValidateCommunities(timerInterval: number): Promise<void> {
logger.info(
`Federation: startValidateCommunities loop with an interval of ${timerInterval} ms...`,
)
// delete all foreign federated community entries to avoid increasing validation efforts and log-files
await DbFederatedCommunity.delete({ foreign: true })
// TODO: replace the timer-loop by an event-based communication to verify announced foreign communities
// better to use setTimeout twice than setInterval once -> see https://javascript.info/settimeout-setinterval
setTimeout(function run() {
@ -78,7 +81,7 @@ async function writeForeignCommunity(
)}`,
)
} else {
let com = await DbCommunity.findOne({ publicKey: dbCom.publicKey })
let com = await DbCommunity.findOneBy({ publicKey: dbCom.publicKey })
if (!com) {
com = DbCommunity.create()
}

View File

@ -1,5 +1,7 @@
import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { ContributionMessageType } from '@enum/ContributionMessageType'
@InputType()
@ArgsType()
export class ContributionMessageArgs {
@ -8,4 +10,7 @@ export class ContributionMessageArgs {
@Field(() => String)
message: string
@Field(() => ContributionMessageType, { defaultValue: ContributionMessageType.DIALOG })
messageType: ContributionMessageType
}

View File

@ -2,6 +2,9 @@ import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export class CreateUserArgs {
@Field(() => String, { nullable: true })
alias?: string | null
@Field(() => String)
email: string

View File

@ -5,12 +5,12 @@ import { Order } from '@enum/Order'
@ArgsType()
export class Paginated {
@Field(() => Int, { nullable: true })
currentPage?: number
@Field(() => Int, { defaultValue: 1 })
currentPage: number
@Field(() => Int, { nullable: true })
pageSize?: number
@Field(() => Int, { defaultValue: 3 })
pageSize: number
@Field(() => Order, { nullable: true })
order?: Order
@Field(() => Order, { defaultValue: Order.DESC })
order: Order
}

View File

@ -0,0 +1,18 @@
import { Field, ArgsType, Int } from 'type-graphql'
import { ContributionStatus } from '@enum/ContributionStatus'
@ArgsType()
export class SearchContributionsFilterArgs {
@Field(() => [ContributionStatus], { nullable: true, defaultValue: null })
statusFilter?: ContributionStatus[] | null
@Field(() => Int, { nullable: true })
userId?: number | null
@Field(() => String, { nullable: true, defaultValue: '' })
query?: string | null
@Field(() => Boolean, { nullable: true })
noHashtag?: boolean | null
}

View File

@ -1,21 +0,0 @@
import { ArgsType, Field, Int } from 'type-graphql'
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
@ArgsType()
export class SearchUsersArgs {
@Field(() => String)
searchText: string
@Field(() => Int, { nullable: true })
// eslint-disable-next-line type-graphql/invalid-nullable-input-type
currentPage?: number
@Field(() => Int, { nullable: true })
// eslint-disable-next-line type-graphql/invalid-nullable-input-type
pageSize?: number
// eslint-disable-next-line type-graphql/wrong-decorator-signature
@Field(() => SearchUsersFilters, { nullable: true, defaultValue: null })
filters?: SearchUsersFilters | null
}

View File

@ -0,0 +1,13 @@
import { ArgsType, Field, Int, InputType } from 'type-graphql'
import { RoleNames } from '@enum/RoleNames'
@InputType()
@ArgsType()
export class SetUserRoleArgs {
@Field(() => Int)
userId: number
@Field(() => RoleNames, { nullable: true })
role: RoleNames | null | undefined
}

View File

@ -1,10 +1,12 @@
import { User } from '@entity/User'
import { AuthChecker } from 'type-graphql'
import { RoleNames } from '@enum/RoleNames'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { decode, encode } from '@/auth/JWT'
import { RIGHTS } from '@/auth/RIGHTS'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN, ROLE_MODERATOR } from '@/auth/ROLES'
import { Context } from '@/server/context'
import { LogError } from '@/server/LogError'
@ -33,10 +35,23 @@ export const isAuthorized: AuthChecker<Context> = async ({ context }, rights) =>
try {
const user = await User.findOneOrFail({
where: { gradidoID: decoded.gradidoID },
relations: ['emailContact'],
withDeleted: true,
relations: ['emailContact', 'userRoles'],
})
context.user = user
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
context.role = ROLE_USER
if (user.userRoles?.length > 0) {
switch (user.userRoles[0].role) {
case RoleNames.ADMIN:
context.role = ROLE_ADMIN
break
case RoleNames.MODERATOR:
context.role = ROLE_MODERATOR
break
default:
context.role = ROLE_USER
}
}
} catch {
// in case the database query fails (user deleted)
throw new LogError('401 Unauthorized')

View File

@ -3,6 +3,7 @@ import { registerEnumType } from 'type-graphql'
export enum ContributionMessageType {
HISTORY = 'HISTORY',
DIALOG = 'DIALOG',
MODERATOR = 'MODERATOR', // messages for moderator communication, can only be seen by moderators
}
registerEnumType(ContributionMessageType, {

View File

@ -0,0 +1,13 @@
import { registerEnumType } from 'type-graphql'
export enum RoleNames {
UNAUTHORIZED = 'UNAUTHORIZED',
USER = 'USER',
MODERATOR = 'MODERATOR',
ADMIN = 'ADMIN',
}
registerEnumType(RoleNames, {
name: 'RoleNames', // this one is mandatory
description: 'Possible role names', // this one is optional
})

View File

@ -6,6 +6,7 @@ export class AdminUser {
constructor(user: User) {
this.firstName = user.firstName
this.lastName = user.lastName
this.role = user.userRoles.length > 0 ? user.userRoles[0].role : ''
}
@Field(() => String)
@ -13,6 +14,9 @@ export class AdminUser {
@Field(() => String)
lastName: string
@Field(() => String)
role: string
}
@ObjectType()

View File

@ -15,7 +15,7 @@ export class Contribution {
this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy
this.contributionDate = contribution.contributionDate
this.state = contribution.contributionStatus
this.status = contribution.contributionStatus
this.messagesCount = contribution.messages ? contribution.messages.length : 0
this.deniedAt = contribution.deniedAt
this.deniedBy = contribution.deniedBy
@ -68,7 +68,7 @@ export class Contribution {
messagesCount: number
@Field(() => String)
state: string
status: string
@Field(() => Int, { nullable: true })
moderatorId: number | null

View File

@ -1,25 +1,39 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Float, Int } from 'type-graphql'
import { GdtEntryType } from '@enum/GdtEntryType'
@ObjectType()
export class GdtEntry {
constructor(json: any) {
this.id = json.id
this.amount = json.amount
this.date = json.date
this.email = json.email
this.comment = json.comment
this.couponCode = json.coupon_code
this.gdtEntryType = json.gdt_entry_type_id
this.factor = json.factor
this.amount2 = json.amount2
this.factor2 = json.factor2
this.gdt = json.gdt
constructor({
id,
amount,
date,
email,
comment,
// eslint-disable-next-line camelcase
coupon_code,
// eslint-disable-next-line camelcase
gdt_entry_type_id,
factor,
amount2,
factor2,
gdt,
}: any) {
this.id = id
this.amount = amount
this.date = date
this.email = email
this.comment = comment
// eslint-disable-next-line camelcase
this.couponCode = coupon_code
// eslint-disable-next-line camelcase
this.gdtEntryType = gdt_entry_type_id
this.factor = factor
this.amount2 = amount2
this.factor2 = factor2
this.gdt = gdt
}
@Field(() => Int)

View File

@ -1,24 +1,19 @@
/* 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-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field, Int, Float } from 'type-graphql'
import { GdtEntry } from './GdtEntry'
@ObjectType()
export class GdtEntryList {
constructor(json: any) {
this.state = json.state
this.count = json.count
this.gdtEntries = json.gdtEntries ? json.gdtEntries.map((json: any) => new GdtEntry(json)) : []
this.gdtSum = json.gdtSum
this.timeUsed = json.timeUsed
constructor(status = '', count = 0, gdtEntries = [], gdtSum = 0, timeUsed = 0) {
this.status = status
this.count = count
this.gdtEntries = gdtEntries
this.gdtSum = gdtSum
this.timeUsed = timeUsed
}
@Field(() => String)
state: string
status: string
@Field(() => Int)
count: number

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