mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 01:46:07 +00:00
Merge remote-tracking branch 'origin/master' into
3030-feature-role-administration-backend
This commit is contained in:
commit
eb537301b8
1
.github/workflows/lint_pr.yml
vendored
1
.github/workflows/lint_pr.yml
vendored
@ -29,6 +29,7 @@ jobs:
|
||||
database
|
||||
release
|
||||
federation
|
||||
dht
|
||||
workflow
|
||||
docker
|
||||
other
|
||||
|
||||
56
CHANGELOG.md
56
CHANGELOG.md
@ -4,8 +4,64 @@ 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.22.0](https://github.com/gradido/gradido/compare/1.21.0...1.22.0)
|
||||
|
||||
- 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)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.21.0",
|
||||
"version": "1.22.0",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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>
|
||||
|
||||
37
admin/src/components/UserQuery.vue
Normal file
37
admin/src/components/UserQuery.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-input-group>
|
||||
<b-form-input
|
||||
type="text"
|
||||
class="test-input-criteria"
|
||||
v-model="currentValue"
|
||||
:placeholder="$t('user_search')"
|
||||
></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: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
if (this.value !== this.currentValue) {
|
||||
this.$emit('input', this.currentValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -7,6 +7,7 @@ export const adminListContributions = gql`
|
||||
$order: Order = DESC
|
||||
$statusFilter: [ContributionStatus!]
|
||||
$userId: Int
|
||||
$query: String
|
||||
) {
|
||||
adminListContributions(
|
||||
currentPage: $currentPage
|
||||
@ -14,6 +15,7 @@ export const adminListContributions = gql`
|
||||
order: $order
|
||||
statusFilter: $statusFilter
|
||||
userId: $userId
|
||||
query: $query
|
||||
) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
@ -26,7 +28,7 @@ export const adminListContributions = gql`
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
state
|
||||
status
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
"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 +108,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",
|
||||
@ -184,7 +189,7 @@
|
||||
"confirmed": "Bestätigt",
|
||||
"memo": "Nachricht",
|
||||
"period": "Zeitraum",
|
||||
"state": "Status",
|
||||
"status": "Status",
|
||||
"submitted": "Eingereicht",
|
||||
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
||||
},
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
"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 +108,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",
|
||||
@ -184,7 +189,7 @@
|
||||
"confirmed": "Confirmed",
|
||||
"memo": "Message",
|
||||
"period": "Period",
|
||||
"state": "State",
|
||||
"status": "State",
|
||||
"submitted": "Submitted",
|
||||
"title": "All creation-transactions for the user"
|
||||
},
|
||||
|
||||
@ -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,
|
||||
@ -341,6 +341,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['CONFIRMED'],
|
||||
})
|
||||
})
|
||||
@ -356,6 +357,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING'],
|
||||
})
|
||||
})
|
||||
@ -372,6 +374,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['DENIED'],
|
||||
})
|
||||
})
|
||||
@ -388,6 +391,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['DELETED'],
|
||||
})
|
||||
})
|
||||
@ -404,6 +408,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
|
||||
})
|
||||
})
|
||||
@ -424,6 +429,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 2,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
|
||||
})
|
||||
})
|
||||
@ -439,6 +445,7 @@ describe('CreationConfirm', () => {
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING'],
|
||||
})
|
||||
})
|
||||
@ -449,14 +456,48 @@ 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,
|
||||
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,
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
|
||||
<template>
|
||||
<div class="creation-confirm">
|
||||
<user-query class="mb-4 mt-2" v-model="query" />
|
||||
<div>
|
||||
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
|
||||
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
|
||||
@ -43,7 +44,7 @@
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
@show-overlay="showOverlay"
|
||||
@update-state="updateStatus"
|
||||
@update-status="updateStatus"
|
||||
@update-contributions="$apollo.queries.ListAllContributions.refetch()"
|
||||
/>
|
||||
|
||||
@ -85,6 +86,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 +105,7 @@ export default {
|
||||
components: {
|
||||
OpenCreationsTable,
|
||||
Overlay,
|
||||
UserQuery,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -114,6 +117,7 @@ export default {
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
query: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -187,7 +191,7 @@ 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') : '—'
|
||||
@ -217,7 +221,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 +258,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 +294,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 +330,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 +367,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 +413,7 @@ export default {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
statusFilter: this.statusFilter,
|
||||
query: this.query,
|
||||
}
|
||||
},
|
||||
fetchPolicy: 'no-cache',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -10,11 +10,20 @@ 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,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 3,
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
creation: [0, 0, 0],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
@ -28,23 +37,14 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
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],
|
||||
emailChecked: true,
|
||||
deletedAt: null,
|
||||
},
|
||||
{
|
||||
userId: 4,
|
||||
firstName: 'New',
|
||||
lastName: 'User',
|
||||
email: 'new@user.ch',
|
||||
creation: [1000, 1000, 1000],
|
||||
emailChecked: false,
|
||||
deletedAt: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -79,9 +79,10 @@ describe('UserSearch', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
query: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filters: {
|
||||
byActivated: null,
|
||||
byDeleted: null,
|
||||
@ -100,9 +101,10 @@ describe('UserSearch', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
query: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filters: {
|
||||
byActivated: false,
|
||||
byDeleted: null,
|
||||
@ -122,9 +124,10 @@ describe('UserSearch', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
query: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filters: {
|
||||
byActivated: null,
|
||||
byDeleted: true,
|
||||
@ -144,9 +147,10 @@ describe('UserSearch', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
searchText: '',
|
||||
query: '',
|
||||
currentPage: 2,
|
||||
pageSize: 25,
|
||||
order: 'DESC',
|
||||
filters: {
|
||||
byActivated: null,
|
||||
byDeleted: null,
|
||||
@ -166,9 +170,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 +186,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,
|
||||
|
||||
@ -23,21 +23,7 @@
|
||||
</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"
|
||||
@ -49,7 +35,7 @@
|
||||
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',
|
||||
})
|
||||
|
||||
@ -21,6 +21,11 @@ KLICKTIPP_PASSWORD=secret321
|
||||
KLICKTIPP_APIKEY_DE=SomeFakeKeyDE
|
||||
KLICKTIPP_APIKEY_EN=SomeFakeKeyEN
|
||||
|
||||
# IOTA
|
||||
IOTA=false
|
||||
IOTA_API_URL=https://chrysalis-nodes.iota.org
|
||||
IOTA_COMMUNITY_ALIAS=GRADIDO: TestHelloWelt1
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
|
||||
@ -22,6 +22,11 @@ KLICKTIPP_PASSWORD=$KLICKTIPP_PASSWORD
|
||||
KLICKTIPP_APIKEY_DE=$KLICKTIPP_APIKEY_DE
|
||||
KLICKTIPP_APIKEY_EN=$KLICKTIPP_APIKEY_EN
|
||||
|
||||
# IOTA
|
||||
IOTA=$IOTA
|
||||
IOTA_API_URL=https://chrysalis-nodes.iota.org
|
||||
IOTA_COMMUNITY_ALIAS=GRADIDO: TestHelloWelt1
|
||||
|
||||
# Community
|
||||
COMMUNITY_NAME=$COMMUNITY_NAME
|
||||
COMMUNITY_URL=$COMMUNITY_URL
|
||||
|
||||
@ -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',
|
||||
|
||||
5
backend/@types/random-bigint/index.d.ts
vendored
Normal file
5
backend/@types/random-bigint/index.d.ts
vendored
Normal 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
|
||||
}
|
||||
8
backend/@types/sodium-native/index.d.ts
vendored
Normal file
8
backend/@types/sodium-native/index.d.ts
vendored
Normal 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
|
||||
}
|
||||
@ -33,6 +33,8 @@ LABEL maintainer="support@gradido.net"
|
||||
# Install Additional Software
|
||||
## install: git
|
||||
#RUN apk --no-cache add git
|
||||
# Install Build Tool for Rust for @iota/client
|
||||
RUN apk add --no-cache rust cargo python3 make g++
|
||||
|
||||
# Settings
|
||||
## Expose Container Port
|
||||
@ -92,6 +94,9 @@ CMD /bin/sh -c "yarn run start"
|
||||
##################################################################################
|
||||
FROM base as production
|
||||
|
||||
# Remove Build Tool for Rust else they would bloat the image unneccessary
|
||||
RUN apk del rust cargo python3 make g++
|
||||
|
||||
# Copy "binary"-files from build image
|
||||
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||
COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.21.0",
|
||||
"version": "1.22.0",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -19,6 +19,7 @@
|
||||
"locales": "scripts/sort.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iota/client": "^2",
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"await-semaphore": "^0.1.3",
|
||||
"axios": "^0.21.1",
|
||||
@ -55,6 +56,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",
|
||||
|
||||
43
backend/src/apis/IotaConnector.test.ts
Normal file
43
backend/src/apis/IotaConnector.test.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import { IotaClientSingleton } from '@/apis/IotaConnector'
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
CONFIG.IOTA = true
|
||||
CONFIG.IOTA_COMMUNITY_ALIAS = 'GRADIDO: TestHelloWelt1'
|
||||
CONFIG.IOTA_API_URL = 'https://chrysalis-nodes.iota.org'
|
||||
|
||||
describe('apis/IotaConnector/enabled', () => {
|
||||
describe('Hello World', () => {
|
||||
const now = new Date()
|
||||
let messageId: string
|
||||
const messageString = 'Hello World - ' + now.toString()
|
||||
const messageHexString = Buffer.from(messageString, 'utf8').toString('hex')
|
||||
const indexHexString = Buffer.from(CONFIG.IOTA_COMMUNITY_ALIAS, 'utf8').toString('hex')
|
||||
it('sends hello world message to iota tangle', async () => {
|
||||
const iotaMessage = await IotaClientSingleton.getInstance()?.sendDataMessage(messageString)
|
||||
expect(iotaMessage).toMatchObject({
|
||||
message: {
|
||||
payload: {
|
||||
data: messageHexString,
|
||||
index: indexHexString,
|
||||
},
|
||||
},
|
||||
messageId: expect.any(String),
|
||||
})
|
||||
messageId =
|
||||
iotaMessage?.messageId ?? '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710'
|
||||
})
|
||||
it('receives hello world message from iota tangle by message id', async () => {
|
||||
const iotaMessage = await IotaClientSingleton.getInstance()?.getMessage(messageId)
|
||||
expect(iotaMessage).toMatchObject({
|
||||
message: {
|
||||
payload: {
|
||||
data: messageHexString,
|
||||
index: indexHexString,
|
||||
},
|
||||
},
|
||||
messageId,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
93
backend/src/apis/IotaConnector.ts
Normal file
93
backend/src/apis/IotaConnector.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Client, ClientBuilder } from '@iota/client'
|
||||
import { MessageWrapper } from '@iota/client/lib/types'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
// 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
|
||||
class IotaClientSingleton {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
private static instance: IotaClientSingleton
|
||||
private client: Client
|
||||
/**
|
||||
* 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(): IotaClientSingleton | undefined {
|
||||
if (!CONFIG.IOTA || !CONFIG.IOTA_API_URL) {
|
||||
logger.info(`Iota are disabled via config...`)
|
||||
return
|
||||
}
|
||||
if (!IotaClientSingleton.instance) {
|
||||
IotaClientSingleton.instance = new IotaClientSingleton()
|
||||
try {
|
||||
IotaClientSingleton.instance.client = new ClientBuilder().node(CONFIG.IOTA_API_URL).build()
|
||||
} catch (e) {
|
||||
logger.error("couldn't connect to iota")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return IotaClientSingleton.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* send data message onto iota tangle
|
||||
* use CONFIG.IOTA_COMMUNITY_ALIAS for index
|
||||
* @param {string} message - the message as utf based string, will be converted to hex automatically from @iota/client
|
||||
* @return {Promise<MessageWrapper>} the iota message typed
|
||||
*/
|
||||
public sendDataMessage(message: string): Promise<MessageWrapper> {
|
||||
return this.client.message().index(CONFIG.IOTA_COMMUNITY_ALIAS).data(message).submit()
|
||||
}
|
||||
|
||||
/**
|
||||
* receive message for known message id from iota tangle
|
||||
* @param {string} messageId - as hex string
|
||||
* @return {Promise<MessageWrapper>} the iota message typed
|
||||
*/
|
||||
public getMessage(messageId: string): Promise<MessageWrapper> {
|
||||
return this.client.getMessage().data(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
export { IotaClientSingleton }
|
||||
|
||||
/**
|
||||
* example for message:
|
||||
```json
|
||||
{
|
||||
message: {
|
||||
networkId: '1454675179895816119',
|
||||
parentMessageIds: [
|
||||
'5f30efecca59fdfef7c103e85ef691b2b1dc474e9eae9056888a6d58605083e7',
|
||||
'77cef2fb405daedcd7469e009bb87a6d9a4840e618cdb599cd21a30a9fec88dc',
|
||||
'7d2cfb39f40585ba568a29ad7e85c1478b2584496eb736d4001ac344f6a6cacf',
|
||||
'c66da602874220dfa26925f6be540d37c0084d37cd04726fcc5be9d80b36f850'
|
||||
],
|
||||
payload: {
|
||||
type: 2,
|
||||
index: '4752414449444f3a205465737448656c6c6f57656c7431',
|
||||
data: '48656c6c6f20576f726c64202d20546875204a756e20303820323032332031343a35393a343520474d542b3030303020284b6f6f7264696e69657274652057656c747a65697429'
|
||||
},
|
||||
nonce: '13835058055282465157'
|
||||
},
|
||||
messageId: '5498130bc3918e1a7143969ce05805502417e3e1bd596d3c44d6a0adeea22710'
|
||||
}
|
||||
```
|
||||
*/
|
||||
26
backend/src/apis/IotaConnector_disabled.test.ts
Normal file
26
backend/src/apis/IotaConnector_disabled.test.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { IotaClientSingleton } from '@/apis/IotaConnector'
|
||||
import { CONFIG } from '@/config'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
describe('apis/IotaConnector/disabled', () => {
|
||||
beforeEach(() => {
|
||||
CONFIG.IOTA = false
|
||||
})
|
||||
it('getInstance return undefined if iota is disabled', () => {
|
||||
const spyLog = jest.spyOn(logger, 'info')
|
||||
expect(IotaClientSingleton.getInstance()).toBeUndefined()
|
||||
expect(spyLog).toHaveBeenCalledWith('Iota are disabled via config...')
|
||||
})
|
||||
})
|
||||
|
||||
describe('apis/IotaConnector/invalidIotaUrl', () => {
|
||||
beforeEach(() => {
|
||||
CONFIG.IOTA = true
|
||||
CONFIG.IOTA_API_URL = 'invalidUrl'
|
||||
})
|
||||
it('log "couldn\'t connect to iota"', () => {
|
||||
const spyLog = jest.spyOn(logger, 'error')
|
||||
expect(IotaClientSingleton.getInstance()).toBeUndefined()
|
||||
expect(spyLog).toHaveBeenCalledWith("couldn't connect to iota")
|
||||
})
|
||||
})
|
||||
@ -52,6 +52,7 @@ 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',
|
||||
|
||||
@ -12,14 +12,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0067-add_user_roles_table',
|
||||
DB_VERSION: '0068-add_user_roles_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: 'v16.2023-06-08',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -51,6 +51,12 @@ const klicktipp = {
|
||||
KLICKTIPP_APIKEY_EN: process.env.KLICKTIPP_APIKEY_EN ?? 'SomeFakeKeyEN',
|
||||
}
|
||||
|
||||
const iota = {
|
||||
IOTA: process.env.IOTA === 'true' || false,
|
||||
IOTA_API_URL: process.env.IOTA_API_URL ?? 'https://chrysalis-nodes.iota.org',
|
||||
IOTA_COMMUNITY_ALIAS: process.env.IOTA_COMMUNITY_ALIAS ?? 'GRADIDO: TestHelloWelt1',
|
||||
}
|
||||
|
||||
const community = {
|
||||
COMMUNITY_NAME: process.env.COMMUNITY_NAME ?? 'Gradido Entwicklung',
|
||||
COMMUNITY_URL: process.env.COMMUNITY_URL ?? 'http://localhost/',
|
||||
@ -126,6 +132,7 @@ export const CONFIG = {
|
||||
...server,
|
||||
...database,
|
||||
...klicktipp,
|
||||
...iota,
|
||||
...community,
|
||||
...email,
|
||||
...loginServer,
|
||||
|
||||
1521
backend/src/emails/__snapshots__/sendEmailVariants.test.ts.snap
Normal file
1521
backend/src/emails/__snapshots__/sendEmailVariants.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
@ -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'),
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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="https://gdd.gradido.net/community/contribution") #{t('emails.general.toAccount')}
|
||||
|
||||
include ../includes/doNotReply.pug
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -0,0 +1,7 @@
|
||||
//-
|
||||
h2= t('emails.general.contributionDetails')
|
||||
div(class="p_content")= t('emails.contribution.toSeeContributionsAndMessages')
|
||||
a.button-3(href="https://gdd.gradido.net/community/contributions") #{t('emails.general.toAccount')}
|
||||
div(class="p_content")= t('emails.general.orCopyLink')
|
||||
|
||||
a.clink(href="https://gdd.gradido.net/community/contributions") https://gdd.gradido.net/community/contributions
|
||||
1
backend/src/emails/templates/includes/doNotReply.pug
Normal file
1
backend/src/emails/templates/includes/doNotReply.pug
Normal file
@ -0,0 +1 @@
|
||||
div(class="p_content")= t('emails.general.pleaseDoNotReply')
|
||||
216
backend/src/emails/templates/includes/email.css
Normal file
216
backend/src/emails/templates/includes/email.css
Normal 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;
|
||||
}
|
||||
BIN
backend/src/emails/templates/includes/facebook-icon.png
Normal file
BIN
backend/src/emails/templates/includes/facebook-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
73
backend/src/emails/templates/includes/footer.pug
Normal file
73
backend/src/emails/templates/includes/footer.pug
Normal 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
|
||||
|
||||
BIN
backend/src/emails/templates/includes/gradido-header.jpeg
Normal file
BIN
backend/src/emails/templates/includes/gradido-header.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
6
backend/src/emails/templates/includes/greeting.pug
Normal file
6
backend/src/emails/templates/includes/greeting.pug
Normal 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')
|
||||
13
backend/src/emails/templates/includes/header.pug
Normal file
13
backend/src/emails/templates/includes/header.pug
Normal 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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
10
backend/src/emails/templates/includes/requestNewLink.pug
Normal file
10
backend/src/emails/templates/includes/requestNewLink.pug
Normal 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')}
|
||||
BIN
backend/src/emails/templates/includes/telegram-icon.png
Normal file
BIN
backend/src/emails/templates/includes/telegram-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
backend/src/emails/templates/includes/twitter-icon.png
Normal file
BIN
backend/src/emails/templates/includes/twitter-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
136
backend/src/emails/templates/includes/webflow.css
Normal file
136
backend/src/emails/templates/includes/webflow.css
Normal 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);
|
||||
}
|
||||
BIN
backend/src/emails/templates/includes/youtube-icon.png
Normal file
BIN
backend/src/emails/templates/includes/youtube-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
26
backend/src/emails/templates/layout.pug
Normal file
26
backend/src/emails/templates/layout.pug
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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="https://gdd.gradido.net/transactions") #{t('emails.general.toAccount')}
|
||||
|
||||
include ../includes/doNotReply.pug
|
||||
|
||||
@ -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="https://gdd.gradido.net/transactions") #{t('emails.general.toAccount')}
|
||||
|
||||
include ../includes/doNotReply.pug
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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, {
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
export class KlickTipp {
|
||||
constructor(json: any) {
|
||||
this.newsletterState = json.status === 'Subscribed'
|
||||
constructor(newsletterState: boolean) {
|
||||
this.newsletterState = newsletterState
|
||||
}
|
||||
|
||||
@Field(() => Boolean)
|
||||
|
||||
@ -16,7 +16,7 @@ export class UnconfirmedContribution {
|
||||
this.email = user ? user.emailContact.email : ''
|
||||
this.moderator = contribution.moderatorId
|
||||
this.creation = creations
|
||||
this.state = contribution.contributionStatus
|
||||
this.status = contribution.contributionStatus
|
||||
this.messageCount = contribution.messages ? contribution.messages.length : 0
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ export class UnconfirmedContribution {
|
||||
creation: Decimal[]
|
||||
|
||||
@Field(() => String)
|
||||
state: string
|
||||
status: string
|
||||
|
||||
@Field(() => Int)
|
||||
messageCount: number
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { IsNull } from '@dbTools/typeorm'
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
|
||||
|
||||
import { Balance } from '@model/Balance'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
@ -15,6 +14,7 @@ import { calculateDecay } from '@/util/decay'
|
||||
|
||||
import { GdtResolver } from './GdtResolver'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { transactionLinkSummary } from './util/transactionLinkSummary'
|
||||
|
||||
@Resolver()
|
||||
export class BalanceResolver {
|
||||
@ -57,7 +57,7 @@ export class BalanceResolver {
|
||||
const linkCount = await dbTransactionLink.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
redeemedAt: null,
|
||||
redeemedAt: IsNull(),
|
||||
// validUntil: MoreThan(new Date()),
|
||||
},
|
||||
})
|
||||
@ -77,10 +77,9 @@ export class BalanceResolver {
|
||||
)
|
||||
|
||||
// The final balance is reduced by the link amount withheld
|
||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||
const { sumHoldAvailableAmount } = context.sumHoldAvailableAmount
|
||||
? { sumHoldAvailableAmount: context.sumHoldAvailableAmount }
|
||||
: await transactionLinkRepository.summary(user.id, now)
|
||||
: await transactionLinkSummary(user.id, now)
|
||||
|
||||
logger.debug(`context.sumHoldAvailableAmount=${context.sumHoldAvailableAmount}`)
|
||||
logger.debug(`sumHoldAvailableAmount=${sumHoldAvailableAmount}`)
|
||||
|
||||
@ -542,7 +542,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('updated the DB record', async () => {
|
||||
await expect(DbContributionLink.findOne(linkId)).resolves.toEqual(
|
||||
await expect(DbContributionLink.findOne({ where: { id: linkId } })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
id: linkId,
|
||||
name: 'Dokumenta 2023',
|
||||
|
||||
@ -103,7 +103,7 @@ export class ContributionLinkResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||
const dbContributionLink = await DbContributionLink.findOne({ where: { id } })
|
||||
if (!dbContributionLink) {
|
||||
throw new LogError('Contribution Link not found', id)
|
||||
}
|
||||
@ -130,7 +130,7 @@ export class ContributionLinkResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionLink> {
|
||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||
const dbContributionLink = await DbContributionLink.findOne({ where: { id } })
|
||||
if (!dbContributionLink) {
|
||||
throw new LogError('Contribution Link not found', id)
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
createContributionMessage,
|
||||
login,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listContributionMessages } from '@/seeds/graphql/queries'
|
||||
import { listContributionMessages, adminListContributionMessages } from '@/seeds/graphql/queries'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
|
||||
@ -217,6 +217,33 @@ describe('ContributionMessageResolver', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('contribution message type MODERATOR', () => {
|
||||
it('creates ContributionMessage', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
message: 'Internal moderator communication',
|
||||
messageType: 'MODERATOR',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
adminCreateContributionMessage: expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Internal moderator communication',
|
||||
type: 'MODERATOR',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -385,7 +412,7 @@ describe('ContributionMessageResolver', () => {
|
||||
resetToken()
|
||||
})
|
||||
|
||||
it('returns a list of contributionmessages', async () => {
|
||||
it('returns a list of contributionmessages without type MODERATOR', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: listContributionMessages,
|
||||
@ -419,4 +446,96 @@ describe('ContributionMessageResolver', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('adminListContributionMessages', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminListContributionMessages,
|
||||
variables: { contributionId: 1 },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as user', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminListContributionMessages,
|
||||
variables: { contributionId: 1 },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated as admin', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
resetToken()
|
||||
})
|
||||
|
||||
it('returns a list of contributionmessages with type MODERATOR', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminListContributionMessages,
|
||||
variables: { contributionId: result.data.createContribution.id },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
adminListContributionMessages: {
|
||||
count: 3,
|
||||
messages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Admin Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'User Test',
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Bibi',
|
||||
userLastName: 'Bloxberg',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
message: 'Internal moderator communication',
|
||||
type: 'MODERATOR',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -8,8 +8,8 @@ import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type
|
||||
|
||||
import { ContributionMessageArgs } from '@arg/ContributionMessageArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { ContributionMessageType } from '@enum/MessageType'
|
||||
import { Order } from '@enum/Order'
|
||||
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
|
||||
|
||||
@ -22,6 +22,8 @@ import {
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { findContributionMessages } from './util/findContributionMessages'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionMessageResolver {
|
||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE])
|
||||
@ -36,7 +38,7 @@ export class ContributionMessageResolver {
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
const contributionMessage = DbContributionMessage.create()
|
||||
try {
|
||||
const contribution = await DbContribution.findOne({ id: contributionId })
|
||||
const contribution = await DbContribution.findOne({ where: { id: contributionId } })
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', contributionId)
|
||||
}
|
||||
@ -82,16 +84,35 @@ export class ContributionMessageResolver {
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionMessageListResult> {
|
||||
const [contributionMessages, count] = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('cm')
|
||||
.from(DbContributionMessage, 'cm')
|
||||
.leftJoinAndSelect('cm.user', 'u')
|
||||
.where({ contributionId })
|
||||
.orderBy('cm.createdAt', order)
|
||||
.limit(pageSize)
|
||||
.offset((currentPage - 1) * pageSize)
|
||||
.getManyAndCount()
|
||||
const [contributionMessages, count] = await findContributionMessages({
|
||||
contributionId,
|
||||
currentPage,
|
||||
pageSize,
|
||||
order,
|
||||
})
|
||||
|
||||
return {
|
||||
count,
|
||||
messages: contributionMessages.map(
|
||||
(message) => new ContributionMessage(message, message.user),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_LIST_ALL_CONTRIBUTION_MESSAGES])
|
||||
@Query(() => ContributionMessageListResult)
|
||||
async adminListContributionMessages(
|
||||
@Arg('contributionId', () => Int) contributionId: number,
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionMessageListResult> {
|
||||
const [contributionMessages, count] = await findContributionMessages({
|
||||
contributionId,
|
||||
currentPage,
|
||||
pageSize,
|
||||
order,
|
||||
showModeratorType: true,
|
||||
})
|
||||
|
||||
return {
|
||||
count,
|
||||
@ -104,7 +125,7 @@ export class ContributionMessageResolver {
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
|
||||
@Mutation(() => ContributionMessage)
|
||||
async adminCreateContributionMessage(
|
||||
@Args() { contributionId, message }: ContributionMessageArgs,
|
||||
@Args() { contributionId, message, messageType }: ContributionMessageArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionMessage> {
|
||||
const moderator = getUser(context)
|
||||
@ -124,7 +145,7 @@ export class ContributionMessageResolver {
|
||||
if (contribution.userId === moderator.id) {
|
||||
throw new LogError('Admin can not answer on his own contribution', contributionId)
|
||||
}
|
||||
if (!contribution.user.emailContact) {
|
||||
if (!contribution.user.emailContact && contribution.user.emailId) {
|
||||
contribution.user.emailContact = await DbUserContact.findOneOrFail({
|
||||
where: { id: contribution.user.emailId },
|
||||
})
|
||||
@ -133,7 +154,7 @@ export class ContributionMessageResolver {
|
||||
contributionMessage.createdAt = new Date()
|
||||
contributionMessage.message = message
|
||||
contributionMessage.userId = moderator.id
|
||||
contributionMessage.type = ContributionMessageType.DIALOG
|
||||
contributionMessage.type = messageType
|
||||
contributionMessage.isModerator = true
|
||||
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
import { Connection, Equal } from '@dbTools/typeorm'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Event as DbEvent } from '@entity/Event'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
@ -457,7 +457,7 @@ describe('ContributionResolver', () => {
|
||||
describe('contribution has wrong status', () => {
|
||||
beforeAll(async () => {
|
||||
const contribution = await Contribution.findOneOrFail({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
where: { id: pendingContribution.data.createContribution.id },
|
||||
})
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
await contribution.save()
|
||||
@ -469,7 +469,7 @@ describe('ContributionResolver', () => {
|
||||
|
||||
afterAll(async () => {
|
||||
const contribution = await Contribution.findOneOrFail({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
where: { id: pendingContribution.data.createContribution.id },
|
||||
})
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
await contribution.save()
|
||||
@ -1092,29 +1092,29 @@ describe('ContributionResolver', () => {
|
||||
contributionCount: 4,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
memo: 'Test PENDING contribution update',
|
||||
amount: '10',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: contributionToDeny.data.createContribution.id,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Test contribution to deny',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: contributionToDelete.data.createContribution.id,
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
memo: 'Test contribution to delete',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: inProgressContribution.data.createContribution.id,
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
@ -1223,47 +1223,47 @@ describe('ContributionResolver', () => {
|
||||
contributionCount: 7,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
id: contributionToConfirm.data.createContribution.id,
|
||||
memo: 'Test contribution to confirm',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
memo: 'Test PENDING contribution update',
|
||||
amount: '10',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: contributionToDeny.data.createContribution.id,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Test contribution to deny',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: inProgressContribution.data.createContribution.id,
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: bibiCreatedContribution.id,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
@ -1288,47 +1288,47 @@ describe('ContributionResolver', () => {
|
||||
contributionCount: 7,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
id: contributionToConfirm.data.createContribution.id,
|
||||
memo: 'Test contribution to confirm',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
memo: 'Test PENDING contribution update',
|
||||
amount: '10',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: contributionToDeny.data.createContribution.id,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Test contribution to deny',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: inProgressContribution.data.createContribution.id,
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: bibiCreatedContribution.id,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
@ -1353,47 +1353,47 @@ describe('ContributionResolver', () => {
|
||||
contributionCount: 7,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
id: contributionToConfirm.data.createContribution.id,
|
||||
memo: 'Test contribution to confirm',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
memo: 'Test PENDING contribution update',
|
||||
amount: '10',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: contributionToDeny.data.createContribution.id,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Test contribution to deny',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: inProgressContribution.data.createContribution.id,
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: bibiCreatedContribution.id,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
@ -1419,33 +1419,33 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
id: contributionToConfirm.data.createContribution.id,
|
||||
memo: 'Test contribution to confirm',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: bibiCreatedContribution.id,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -1468,20 +1468,20 @@ describe('ContributionResolver', () => {
|
||||
contributionCount: 1,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
memo: 'Test PENDING contribution update',
|
||||
amount: '10',
|
||||
}),
|
||||
@ -1506,20 +1506,20 @@ describe('ContributionResolver', () => {
|
||||
contributionCount: 1,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: inProgressContribution.data.createContribution.id,
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
amount: '100',
|
||||
}),
|
||||
@ -1545,27 +1545,27 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: contributionToDeny.data.createContribution.id,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Test contribution to deny',
|
||||
amount: '100',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -1608,36 +1608,36 @@ describe('ContributionResolver', () => {
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
id: contributionToConfirm.data.createContribution.id,
|
||||
memo: 'Test contribution to confirm',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: pendingContribution.data.createContribution.id,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
memo: 'Test PENDING contribution update',
|
||||
amount: '10',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: bibiCreatedContribution.id,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '1000',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
memo: 'Whatever contribution',
|
||||
amount: '166',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -1828,7 +1828,7 @@ describe('ContributionResolver', () => {
|
||||
creation = await Contribution.findOneOrFail({
|
||||
where: {
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: 400,
|
||||
amount: Equal(new Decimal('400')),
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -2691,7 +2691,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(50),
|
||||
@ -2700,7 +2700,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(450),
|
||||
@ -2709,7 +2709,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(400),
|
||||
@ -2718,7 +2718,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
@ -2727,7 +2727,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'der Baumeister',
|
||||
memo: 'Confirmed Contribution',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
@ -2736,7 +2736,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
@ -2745,7 +2745,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Aktives Grundeinkommen',
|
||||
messagesCount: 0,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
@ -2754,7 +2754,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
messagesCount: 0,
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
@ -2763,7 +2763,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
@ -2772,7 +2772,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
@ -2781,7 +2781,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
@ -2790,7 +2790,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test contribution to delete',
|
||||
messagesCount: 0,
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
@ -2799,7 +2799,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test contribution to deny',
|
||||
messagesCount: 0,
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
@ -2808,7 +2808,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test contribution to confirm',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
@ -2817,7 +2817,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test IN_PROGRESS contribution',
|
||||
messagesCount: 1,
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(10),
|
||||
@ -2826,7 +2826,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Test PENDING contribution update',
|
||||
messagesCount: 1,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(1000),
|
||||
@ -2835,7 +2835,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Bloxberg',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
@ -2864,7 +2864,7 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: '100',
|
||||
@ -2873,23 +2873,114 @@ describe('ContributionResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
state: 'PENDING',
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DENIED',
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'DELETED',
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'CONFIRMED',
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
state: 'IN_PROGRESS',
|
||||
status: 'IN_PROGRESS',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
describe('with user query', () => {
|
||||
it('returns only contributions of the queried user', async () => {
|
||||
const {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: 'Peter',
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(3)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 3,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(400),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(100),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Test env contribution',
|
||||
messagesCount: 0,
|
||||
status: 'PENDING',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(200),
|
||||
firstName: 'Peter',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Lustig',
|
||||
memo: 'Das war leider zu Viel!',
|
||||
messagesCount: 0,
|
||||
status: 'DELETED',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
|
||||
// test for case sensitivity and email
|
||||
it('returns only contributions of the queried user email', async () => {
|
||||
const {
|
||||
data: { adminListContributions: contributionListObject },
|
||||
} = await query({
|
||||
query: adminListContributions,
|
||||
variables: {
|
||||
query: 'RAEUBER', // only found in lowercase in the email
|
||||
},
|
||||
})
|
||||
expect(contributionListObject.contributionList).toHaveLength(3)
|
||||
expect(contributionListObject).toMatchObject({
|
||||
contributionCount: 3,
|
||||
contributionList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'DENIED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'DELETED',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: expect.decimalEqual(166),
|
||||
firstName: 'Räuber',
|
||||
id: expect.any(Number),
|
||||
lastName: 'Hotzenplotz',
|
||||
memo: 'Whatever contribution',
|
||||
messagesCount: 0,
|
||||
status: 'CONFIRMED',
|
||||
}),
|
||||
]),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,9 +11,9 @@ import { AdminCreateContributionArgs } from '@arg/AdminCreateContributionArgs'
|
||||
import { AdminUpdateContributionArgs } from '@arg/AdminUpdateContributionArgs'
|
||||
import { ContributionArgs } from '@arg/ContributionArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { ContributionType } from '@enum/ContributionType'
|
||||
import { ContributionMessageType } from '@enum/MessageType'
|
||||
import { Order } from '@enum/Order'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
@ -101,7 +101,7 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
const contribution = await DbContribution.findOne({ where: { id } })
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
@ -138,7 +138,7 @@ export class ContributionResolver {
|
||||
currentPage,
|
||||
pageSize,
|
||||
withDeleted: true,
|
||||
relations: ['messages'],
|
||||
relations: { messages: true },
|
||||
userId: user.id,
|
||||
statusFilter,
|
||||
})
|
||||
@ -160,7 +160,7 @@ export class ContributionResolver {
|
||||
order,
|
||||
currentPage,
|
||||
pageSize,
|
||||
relations: ['user'],
|
||||
relations: { user: true },
|
||||
statusFilter,
|
||||
})
|
||||
|
||||
@ -372,6 +372,8 @@ export class ContributionResolver {
|
||||
statusFilter?: ContributionStatus[] | null,
|
||||
@Arg('userId', () => Int, { nullable: true })
|
||||
userId?: number | null,
|
||||
@Arg('query', () => String, { nullable: true })
|
||||
query?: string | null,
|
||||
): Promise<ContributionListResult> {
|
||||
const [dbContributions, count] = await findContributions({
|
||||
order,
|
||||
@ -379,8 +381,14 @@ export class ContributionResolver {
|
||||
pageSize,
|
||||
withDeleted: true,
|
||||
userId,
|
||||
relations: ['user', 'messages'],
|
||||
relations: {
|
||||
user: {
|
||||
emailContact: true,
|
||||
},
|
||||
messages: true,
|
||||
},
|
||||
statusFilter,
|
||||
query,
|
||||
})
|
||||
|
||||
return new ContributionListResult(
|
||||
@ -395,7 +403,7 @@ export class ContributionResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
const contribution = await DbContribution.findOne({ where: { id } })
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
@ -409,10 +417,10 @@ export class ContributionResolver {
|
||||
) {
|
||||
throw new LogError('Own contribution can not be deleted as admin')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
const user = await DbUser.findOneOrFail({
|
||||
where: { id: contribution.userId },
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
contribution.deletedBy = moderator.id
|
||||
await contribution.save()
|
||||
@ -447,7 +455,7 @@ export class ContributionResolver {
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
try {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
const contribution = await DbContribution.findOne({ where: { id } })
|
||||
if (!contribution) {
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
@ -461,10 +469,11 @@ export class ContributionResolver {
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
throw new LogError('Moderator can not confirm own contribution')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
const user = await DbUser.findOneOrFail({
|
||||
where: { id: contribution.userId },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
if (user.deletedAt) {
|
||||
throw new LogError('Can not confirm contribution since the user was deleted')
|
||||
}
|
||||
@ -565,9 +574,11 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
id,
|
||||
confirmedAt: IsNull(),
|
||||
deniedBy: IsNull(),
|
||||
where: {
|
||||
id,
|
||||
confirmedAt: IsNull(),
|
||||
deniedBy: IsNull(),
|
||||
},
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
throw new LogError('Contribution not found', id)
|
||||
@ -582,10 +593,10 @@ export class ContributionResolver {
|
||||
)
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
const user = await DbUser.findOne(
|
||||
{ id: contributionToUpdate.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
const user = await DbUser.findOne({
|
||||
where: { id: contributionToUpdate.userId },
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
if (!user) {
|
||||
throw new LogError('Could not find User of the Contribution', contributionToUpdate.userId)
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { Resolver, Query, Args, Ctx, Authorized, Arg, Int, Float } from 'type-gr
|
||||
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { Order } from '@enum/Order'
|
||||
import { GdtEntry } from '@model/GdtEntry'
|
||||
import { GdtEntryList } from '@model/GdtEntryList'
|
||||
|
||||
import { apiGet, apiPost } from '@/apis/HttpRequest'
|
||||
@ -31,9 +32,17 @@ export class GdtResolver {
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
||||
)
|
||||
if (!resultGDT.success) {
|
||||
throw new LogError(resultGDT.data)
|
||||
return new GdtEntryList()
|
||||
}
|
||||
return new GdtEntryList(resultGDT.data)
|
||||
const { state, count, gdtEntries, gdtSum, timeUsed } = resultGDT.data
|
||||
return new GdtEntryList(
|
||||
state,
|
||||
count,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
|
||||
gdtEntries ? gdtEntries.map((data: any) => new GdtEntry(data)) : [],
|
||||
gdtSum,
|
||||
timeUsed,
|
||||
)
|
||||
} catch (err) {
|
||||
throw new LogError('GDT Server is not reachable')
|
||||
}
|
||||
|
||||
@ -72,10 +72,10 @@ describe('KlicktippResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the NEWSLETTER_SUBSCRIBE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.NEWSLETTER_SUBSCRIBE,
|
||||
@ -121,10 +121,10 @@ describe('KlicktippResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the NEWSLETTER_UNSUBSCRIBE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.NEWSLETTER_UNSUBSCRIBE,
|
||||
|
||||
@ -456,10 +456,10 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the CONTRIBUTION_LINK_REDEEM event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.CONTRIBUTION_LINK_REDEEM,
|
||||
@ -611,10 +611,10 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the TRANSACTION_LINK_CREATE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.TRANSACTION_LINK_CREATE,
|
||||
@ -664,10 +664,10 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the TRANSACTION_LINK_DELETE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.TRANSACTION_LINK_DELETE,
|
||||
@ -719,14 +719,14 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the TRANSACTION_LINK_REDEEM event in the database', async () => {
|
||||
const creator = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const redeemer = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const creator = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
const redeemer = await UserContact.findOneOrFail({
|
||||
where: { email: 'peter@lustig.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.TRANSACTION_LINK_REDEEM,
|
||||
|
||||
@ -33,6 +33,7 @@ import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { fullName } from '@/util/utilities'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
@ -80,6 +81,7 @@ export class TransactionLinkResolver {
|
||||
|
||||
// validate amount
|
||||
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
|
||||
|
||||
if (!sendBalance) {
|
||||
throw new LogError('User has not enough GDD', user.id)
|
||||
}
|
||||
@ -108,7 +110,7 @@ export class TransactionLinkResolver {
|
||||
): Promise<boolean> {
|
||||
const user = getUser(context)
|
||||
|
||||
const transactionLink = await DbTransactionLink.findOne({ id })
|
||||
const transactionLink = await DbTransactionLink.findOne({ where: { id } })
|
||||
if (!transactionLink) {
|
||||
throw new LogError('Transaction link not found', id)
|
||||
}
|
||||
@ -138,17 +140,22 @@ export class TransactionLinkResolver {
|
||||
@Query(() => QueryLinkResult)
|
||||
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
|
||||
if (code.match(/^CL-/)) {
|
||||
const contributionLink = await DbContributionLink.findOneOrFail(
|
||||
{ code: code.replace('CL-', '') },
|
||||
{ withDeleted: true },
|
||||
)
|
||||
const contributionLink = await DbContributionLink.findOneOrFail({
|
||||
where: { code: code.replace('CL-', '') },
|
||||
withDeleted: true,
|
||||
})
|
||||
return new ContributionLink(contributionLink)
|
||||
} else {
|
||||
const transactionLink = await DbTransactionLink.findOneOrFail({ code }, { withDeleted: true })
|
||||
const user = await DbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
const transactionLink = await DbTransactionLink.findOneOrFail({
|
||||
where: { code },
|
||||
withDeleted: true,
|
||||
})
|
||||
const user = await DbUser.findOneOrFail({ where: { id: transactionLink.userId } })
|
||||
let redeemedBy: User | null = null
|
||||
if (transactionLink?.redeemedBy) {
|
||||
redeemedBy = new User(await DbUser.findOneOrFail({ id: transactionLink.redeemedBy }))
|
||||
redeemedBy = new User(
|
||||
await DbUser.findOneOrFail({ where: { id: transactionLink.redeemedBy } }),
|
||||
)
|
||||
}
|
||||
return new TransactionLink(transactionLink, new User(user), redeemedBy)
|
||||
}
|
||||
@ -191,7 +198,7 @@ export class TransactionLinkResolver {
|
||||
throw new LogError('Contribution link is no longer valid', contributionLink.validTo)
|
||||
}
|
||||
}
|
||||
let alreadyRedeemed: DbContribution | undefined
|
||||
let alreadyRedeemed: DbContribution | null
|
||||
switch (contributionLink.cycle) {
|
||||
case ContributionCycleType.ONCE: {
|
||||
alreadyRedeemed = await queryRunner.manager
|
||||
@ -302,49 +309,51 @@ export class TransactionLinkResolver {
|
||||
return true
|
||||
} else {
|
||||
const now = new Date()
|
||||
const transactionLink = await DbTransactionLink.findOne({ code })
|
||||
if (!transactionLink) {
|
||||
throw new LogError('Transaction link not found', code)
|
||||
const releaseLinkLock = await TRANSACTION_LINK_LOCK.acquire()
|
||||
try {
|
||||
const transactionLink = await DbTransactionLink.findOne({ where: { code } })
|
||||
if (!transactionLink) {
|
||||
throw new LogError('Transaction link not found', code)
|
||||
}
|
||||
|
||||
const linkedUser = await DbUser.findOne({
|
||||
where: {
|
||||
id: transactionLink.userId,
|
||||
},
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
|
||||
if (!linkedUser) {
|
||||
throw new LogError('Linked user not found for given link', transactionLink.userId)
|
||||
}
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new LogError('Cannot redeem own transaction link', user.id)
|
||||
}
|
||||
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
|
||||
}
|
||||
await executeTransaction(
|
||||
transactionLink.amount,
|
||||
transactionLink.memo,
|
||||
linkedUser,
|
||||
user,
|
||||
transactionLink,
|
||||
)
|
||||
await EVENT_TRANSACTION_LINK_REDEEM(
|
||||
user,
|
||||
{ id: transactionLink.userId } as DbUser,
|
||||
transactionLink,
|
||||
transactionLink.amount,
|
||||
)
|
||||
} finally {
|
||||
releaseLinkLock()
|
||||
}
|
||||
|
||||
const linkedUser = await DbUser.findOne(
|
||||
{ id: transactionLink.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
|
||||
if (!linkedUser) {
|
||||
throw new LogError('Linked user not found for given link', transactionLink.userId)
|
||||
}
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new LogError('Cannot redeem own transaction link', user.id)
|
||||
}
|
||||
|
||||
// TODO: The now check should be done within the semaphore lock,
|
||||
// since the program might wait a while till it is ready to proceed
|
||||
// writing the transaction.
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
|
||||
}
|
||||
|
||||
if (transactionLink.redeemedBy) {
|
||||
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
|
||||
}
|
||||
|
||||
await executeTransaction(
|
||||
transactionLink.amount,
|
||||
transactionLink.memo,
|
||||
linkedUser,
|
||||
user,
|
||||
transactionLink,
|
||||
)
|
||||
await EVENT_TRANSACTION_LINK_REDEEM(
|
||||
user,
|
||||
{ id: transactionLink.userId } as DbUser,
|
||||
transactionLink,
|
||||
transactionLink.amount,
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -378,7 +387,7 @@ export class TransactionLinkResolver {
|
||||
@Arg('userId', () => Int)
|
||||
userId: number,
|
||||
): Promise<TransactionLinkResult> {
|
||||
const user = await DbUser.findOne({ id: userId })
|
||||
const user = await DbUser.findOne({ where: { id: userId } })
|
||||
if (!user) {
|
||||
throw new LogError('Could not find requested User', userId)
|
||||
}
|
||||
|
||||
@ -346,8 +346,10 @@ describe('send coins', () => {
|
||||
it('stores the TRANSACTION_SEND event in the database', async () => {
|
||||
// Find the exact transaction (sent one is the one with user[1] as user)
|
||||
const transaction = await Transaction.find({
|
||||
userId: user[1].id,
|
||||
memo: 'unrepeatable memo',
|
||||
where: {
|
||||
userId: user[1].id,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
})
|
||||
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
@ -364,8 +366,10 @@ describe('send coins', () => {
|
||||
it('stores the TRANSACTION_RECEIVE event in the database', async () => {
|
||||
// Find the exact transaction (received one is the one with user[0] as user)
|
||||
const transaction = await Transaction.find({
|
||||
userId: user[0].id,
|
||||
memo: 'unrepeatable memo',
|
||||
where: {
|
||||
userId: user[0].id,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
})
|
||||
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
/* eslint-disable new-cap */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
import { getCustomRepository, getConnection, In } from '@dbTools/typeorm'
|
||||
import { getConnection, In } from '@dbTools/typeorm'
|
||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
@ -16,7 +16,6 @@ import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionList } from '@model/TransactionList'
|
||||
import { User } from '@model/User'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import {
|
||||
@ -38,6 +37,7 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
import { getTransactionList } from './util/getTransactionList'
|
||||
import { transactionLinkSummary } from './util/transactionLinkSummary'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -245,9 +245,8 @@ export class TransactionResolver {
|
||||
const self = new User(user)
|
||||
const transactions: Transaction[] = []
|
||||
|
||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, transactionLinkcount } =
|
||||
await transactionLinkRepository.summary(user.id, now)
|
||||
await transactionLinkSummary(user.id, now)
|
||||
context.linkCount = transactionLinkcount
|
||||
logger.debug(`transactionLinkcount=${transactionLinkcount}`)
|
||||
context.sumHoldAvailableAmount = sumHoldAvailableAmount
|
||||
|
||||
@ -197,10 +197,12 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the USER_REGISTER event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: {
|
||||
email: 'peter@lustig.de',
|
||||
},
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.USER_REGISTER,
|
||||
@ -273,10 +275,10 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the EMAIL_ACCOUNT_MULTIREGISTRATION event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'peter@lustig.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_ACCOUNT_MULTIREGISTRATION,
|
||||
@ -294,7 +296,7 @@ describe('UserResolver', () => {
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
||||
})
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
|
||||
UserContact.findOne({ where: { email: 'bibi@bloxberg.de' }, relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
@ -379,7 +381,7 @@ describe('UserResolver', () => {
|
||||
|
||||
it('sets the contribution link id', async () => {
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }),
|
||||
UserContact.findOne({ where: { email: 'ein@besucher.de' }, relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
user: expect.objectContaining({
|
||||
@ -462,8 +464,7 @@ describe('UserResolver', () => {
|
||||
memo: `testing transaction link`,
|
||||
})
|
||||
|
||||
transactionLink = await TransactionLink.findOneOrFail()
|
||||
console.log('transactionLink=', transactionLink)
|
||||
transactionLink = await TransactionLink.findOneOrFail({ where: { userId: bob.id } })
|
||||
|
||||
resetToken()
|
||||
|
||||
@ -481,7 +482,7 @@ describe('UserResolver', () => {
|
||||
|
||||
it('sets the referrer id to bob baumeister id', async () => {
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }),
|
||||
UserContact.findOne({ where: { email: 'which@ever.de' }, relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
user: expect.objectContaining({ referrerId: bob.data.login.id }),
|
||||
@ -548,16 +549,18 @@ describe('UserResolver', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||
const emailContact = await UserContact.findOneOrFail({
|
||||
where: { email: createUserVariables.email },
|
||||
})
|
||||
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||
})
|
||||
newUser = await User.findOneOrFail(
|
||||
{ id: emailContact.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
newUser = await User.findOneOrFail({
|
||||
where: { id: emailContact.userId },
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -590,7 +593,9 @@ describe('UserResolver', () => {
|
||||
describe('no valid password', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||
const emailContact = await UserContact.findOneOrFail({
|
||||
where: { email: createUserVariables.email },
|
||||
})
|
||||
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||
})
|
||||
|
||||
@ -716,10 +721,10 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the USER_LOGIN event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.USER_LOGIN,
|
||||
@ -898,10 +903,10 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the USER_LOGOUT event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.USER_LOGOUT,
|
||||
@ -1066,10 +1071,10 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the EMAIL_FORGOT_PASSWORD event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_FORGOT_PASSWORD,
|
||||
@ -1102,7 +1107,7 @@ describe('UserResolver', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
|
||||
emailContact = await UserContact.findOneOrFail({ where: { email: bibiBloxberg.email } })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -1119,7 +1124,9 @@ describe('UserResolver', () => {
|
||||
errors: [
|
||||
// keep Whitspace in error message!
|
||||
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
|
||||
"emailVerificationCode": "not-valid"
|
||||
"where": {
|
||||
"emailVerificationCode": "not-valid"
|
||||
}
|
||||
}`),
|
||||
],
|
||||
}),
|
||||
@ -1194,20 +1201,20 @@ describe('UserResolver', () => {
|
||||
locale: 'en',
|
||||
},
|
||||
})
|
||||
await expect(User.findOne()).resolves.toEqual(
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
firstName: 'Benjamin',
|
||||
lastName: 'Blümchen',
|
||||
language: 'en',
|
||||
}),
|
||||
)
|
||||
])
|
||||
})
|
||||
|
||||
it('stores the USER_INFO_UPDATE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.USER_INFO_UPDATE,
|
||||
@ -1231,11 +1238,11 @@ describe('UserResolver', () => {
|
||||
alias: 'bibi_Bloxberg',
|
||||
},
|
||||
})
|
||||
await expect(User.findOne()).resolves.toEqual(
|
||||
await expect(User.find()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
alias: 'bibi_Bloxberg',
|
||||
}),
|
||||
)
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1452,10 +1459,10 @@ describe('UserResolver', () => {
|
||||
|
||||
let bibi: User
|
||||
beforeAll(async () => {
|
||||
const usercontact = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const usercontact = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
bibi = usercontact.user
|
||||
bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL
|
||||
bibi.password = SecretKeyCryptographyCreateKey(
|
||||
@ -1469,10 +1476,10 @@ describe('UserResolver', () => {
|
||||
it('changes to gradidoID on login', async () => {
|
||||
await mutate({ mutation: login, variables })
|
||||
|
||||
const usercontact = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const usercontact = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
bibi = usercontact.user
|
||||
|
||||
expect(bibi).toEqual(
|
||||
@ -1618,7 +1625,7 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the ADMIN_USER_ROLE_SET event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
const userContact = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
@ -1629,7 +1636,7 @@ describe('UserResolver', () => {
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.ADMIN_USER_ROLE_SET,
|
||||
affectedUserId: userConatct.user.id,
|
||||
affectedUserId: userContact.user.id,
|
||||
actingUserId: adminContact.user.id,
|
||||
}),
|
||||
)
|
||||
@ -1823,14 +1830,15 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the ADMIN_USER_DELETE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'], withDeleted: true },
|
||||
)
|
||||
const adminConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
withDeleted: true,
|
||||
})
|
||||
const adminConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'peter@lustig.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.ADMIN_USER_DELETE,
|
||||
@ -1974,10 +1982,10 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('sends an account activation email', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
userConatct.emailVerificationCode.toString(),
|
||||
@ -1996,10 +2004,10 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the EMAIL_ADMIN_CONFIRMATION event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.EMAIL_ADMIN_CONFIRMATION,
|
||||
@ -2117,14 +2125,14 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
it('stores the ADMIN_USER_UNDELETE event in the database', async () => {
|
||||
const userConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'bibi@bloxberg.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const adminConatct = await UserContact.findOneOrFail(
|
||||
{ email: 'peter@lustig.de' },
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
const userConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'bibi@bloxberg.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
const adminConatct = await UserContact.findOneOrFail({
|
||||
where: { email: 'peter@lustig.de' },
|
||||
relations: ['user'],
|
||||
})
|
||||
await expect(DbEvent.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventType.ADMIN_USER_UNDELETE,
|
||||
@ -2141,7 +2149,7 @@ describe('UserResolver', () => {
|
||||
|
||||
describe('search users', () => {
|
||||
const variablesWithoutTextAndFilters = {
|
||||
searchText: '',
|
||||
query: '',
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
filters: null,
|
||||
|
||||
@ -2,29 +2,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { getConnection, getCustomRepository, In, IsNull, Not } from '@dbTools/typeorm'
|
||||
import { getConnection, In, IsNull, Not } from '@dbTools/typeorm'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { UserRole } from '@entity/UserRole'
|
||||
import i18n from 'i18n'
|
||||
import {
|
||||
Resolver,
|
||||
Query,
|
||||
Args,
|
||||
Arg,
|
||||
Authorized,
|
||||
Ctx,
|
||||
UseMiddleware,
|
||||
Mutation,
|
||||
Int,
|
||||
} from 'type-graphql'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, Mutation, Int } from 'type-graphql'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { CreateUserArgs } from '@arg/CreateUserArgs'
|
||||
import { Paginated } from '@arg/Paginated'
|
||||
import { SearchUsersArgs } from '@arg/SearchUsersArgs'
|
||||
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
|
||||
import { UnsecureLoginArgs } from '@arg/UnsecureLoginArgs'
|
||||
import { UpdateUserInfosArgs } from '@arg/UpdateUserInfosArgs'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
@ -34,7 +24,6 @@ import { UserContactType } from '@enum/UserContactType'
|
||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
import { User } from '@model/User'
|
||||
import { UserAdmin, SearchUsersResult } from '@model/UserAdmin'
|
||||
import { UserRepository } from '@repository/User'
|
||||
|
||||
import { subscribe } from '@/apis/KlicktippController'
|
||||
import { encode } from '@/auth/JWT'
|
||||
@ -62,7 +51,6 @@ import {
|
||||
EVENT_ADMIN_USER_DELETE,
|
||||
EVENT_ADMIN_USER_UNDELETE,
|
||||
} from '@/event/Events'
|
||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||
import { isValidPassword } from '@/password/EncryptorUtils'
|
||||
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
|
||||
@ -72,17 +60,17 @@ import { communityDbUser } from '@/util/communityUser'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { getTimeDurationObject, printTimeDuration } from '@/util/time'
|
||||
|
||||
import random from 'random-bigint'
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
|
||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { findUserByIdentifier } from './util/findUserByIdentifier'
|
||||
import { findUsers } from './util/findUsers'
|
||||
import { getKlicktippState } from './util/getKlicktippState'
|
||||
import { validateAlias } from './util/validateAlias'
|
||||
import { ArrayMinSize } from 'class-validator'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
|
||||
const random = require('random-bigint')
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
|
||||
const sodium = require('sodium-native')
|
||||
|
||||
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
|
||||
const DEFAULT_LANGUAGE = 'de'
|
||||
const isLanguage = (language: string): boolean => {
|
||||
@ -97,13 +85,13 @@ const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
emailContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
emailContact.emailVerificationCode = random(64).toString()
|
||||
logger.debug('newEmailContact...successful', emailContact)
|
||||
return emailContact
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const activationLink = (verificationCode: BigInt): string => {
|
||||
export const activationLink = (verificationCode: string): string => {
|
||||
logger.debug(`activationLink(${verificationCode})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||
}
|
||||
@ -125,7 +113,6 @@ const newGradidoID = async (): Promise<string> => {
|
||||
export class UserResolver {
|
||||
@Authorized([RIGHTS.VERIFY_LOGIN])
|
||||
@Query(() => User)
|
||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||
async verifyLogin(@Ctx() context: Context): Promise<User> {
|
||||
logger.info('verifyLogin...')
|
||||
// TODO refactor and do not have duplicate code with login(see below)
|
||||
@ -135,12 +122,12 @@ export class UserResolver {
|
||||
user.hasElopage = await this.hasElopage(context)
|
||||
|
||||
logger.debug(`verifyLogin... successful: ${user.firstName}.${user.lastName}`)
|
||||
user.klickTipp = await getKlicktippState(userEntity.emailContact.email)
|
||||
return user
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LOGIN])
|
||||
@Mutation(() => User)
|
||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||
async login(
|
||||
@Args() { email, password, publisherId }: UnsecureLoginArgs,
|
||||
@Ctx() context: Context,
|
||||
@ -186,6 +173,7 @@ export class UserResolver {
|
||||
dbUser.publisherId = publisherId
|
||||
await DbUser.save(dbUser)
|
||||
}
|
||||
user.klickTipp = await getKlicktippState(dbUser.emailContact.email)
|
||||
|
||||
context.setHeaders.push({
|
||||
key: 'token',
|
||||
@ -210,7 +198,15 @@ export class UserResolver {
|
||||
@Mutation(() => User)
|
||||
async createUser(
|
||||
@Args()
|
||||
{ email, firstName, lastName, language, publisherId = null, redeemCode = null }: CreateUserArgs,
|
||||
{
|
||||
alias = null,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
language,
|
||||
publisherId = null,
|
||||
redeemCode = null,
|
||||
}: CreateUserArgs,
|
||||
): Promise<User> {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.info(
|
||||
@ -240,12 +236,15 @@ export class UserResolver {
|
||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||
|
||||
const user = new User(communityDbUser)
|
||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||
user.id = randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
||||
user.gradidoID = uuidv4()
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
user.publisherId = publisherId
|
||||
if (alias && (await validateAlias(alias))) {
|
||||
user.alias = alias
|
||||
}
|
||||
logger.debug('partly faked user', user)
|
||||
|
||||
void sendAccountMultiRegistrationEmail({
|
||||
@ -279,13 +278,16 @@ export class UserResolver {
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.language = language
|
||||
if (alias && (await validateAlias(alias))) {
|
||||
dbUser.alias = alias
|
||||
}
|
||||
dbUser.publisherId = publisherId ?? 0
|
||||
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
|
||||
logger.debug('new dbUser', dbUser)
|
||||
if (redeemCode) {
|
||||
if (redeemCode.match(/^CL-/)) {
|
||||
const contributionLink = await DbContributionLink.findOne({
|
||||
code: redeemCode.replace('CL-', ''),
|
||||
where: { code: redeemCode.replace('CL-', '') },
|
||||
})
|
||||
logger.info('redeemCode found contributionLink', contributionLink)
|
||||
if (contributionLink) {
|
||||
@ -293,7 +295,7 @@ export class UserResolver {
|
||||
eventRegisterRedeem.involvedContributionLink = contributionLink
|
||||
}
|
||||
} else {
|
||||
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
|
||||
const transactionLink = await DbTransactionLink.findOne({ where: { code: redeemCode } })
|
||||
logger.info('redeemCode found transactionLink', transactionLink)
|
||||
if (transactionLink) {
|
||||
dbUser.referrerId = transactionLink.userId
|
||||
@ -367,7 +369,8 @@ export class UserResolver {
|
||||
const user = await findUserByEmail(email).catch(() => {
|
||||
logger.warn(`fail on find UserContact per ${email}`)
|
||||
})
|
||||
if (!user) {
|
||||
|
||||
if (!user || user.deletedAt) {
|
||||
logger.warn(`no user found with ${email}`)
|
||||
return true
|
||||
}
|
||||
@ -380,7 +383,7 @@ export class UserResolver {
|
||||
|
||||
user.emailContact.updatedAt = new Date()
|
||||
user.emailContact.emailResendCount++
|
||||
user.emailContact.emailVerificationCode = random(64)
|
||||
user.emailContact.emailVerificationCode = random(64).toString()
|
||||
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
|
||||
await user.emailContact.save().catch(() => {
|
||||
throw new LogError('Unable to save email verification code', user.emailContact)
|
||||
@ -418,10 +421,10 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
// load code
|
||||
const userContact = await DbUserContact.findOneOrFail(
|
||||
{ emailVerificationCode: code },
|
||||
{ relations: ['user'] },
|
||||
).catch(() => {
|
||||
const userContact = await DbUserContact.findOneOrFail({
|
||||
where: { emailVerificationCode: code },
|
||||
relations: ['user'],
|
||||
}).catch(() => {
|
||||
throw new LogError('Could not login with emailVerificationCode')
|
||||
})
|
||||
logger.debug('userContact loaded...')
|
||||
@ -489,7 +492,9 @@ export class UserResolver {
|
||||
@Query(() => Boolean)
|
||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||
logger.info(`queryOptIn(${optIn})...`)
|
||||
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||
const userContact = await DbUserContact.findOneOrFail({
|
||||
where: { emailVerificationCode: optIn },
|
||||
})
|
||||
logger.debug('found optInCode', userContact)
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
||||
@ -616,11 +621,7 @@ export class UserResolver {
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||
): Promise<SearchAdminUsersResult> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
console.log('test')
|
||||
|
||||
const [users, count] = await userRepository.findAndCount({
|
||||
relations: ['userRoles'],
|
||||
const [users, count] = await DbUser.findAndCount({
|
||||
where: {
|
||||
userRoles: { role: In(['admin', 'moderator']) },
|
||||
},
|
||||
@ -645,12 +646,14 @@ export class UserResolver {
|
||||
@Authorized([RIGHTS.SEARCH_USERS])
|
||||
@Query(() => SearchUsersResult)
|
||||
async searchUsers(
|
||||
@Arg('query', () => String) query: string,
|
||||
@Arg('filters', () => SearchUsersFilters, { nullable: true })
|
||||
filters: SearchUsersFilters | null | undefined,
|
||||
@Args()
|
||||
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||
{ currentPage = 1, pageSize = 25, order = Order.ASC }: Paginated,
|
||||
@Ctx() context: Context,
|
||||
): Promise<SearchUsersResult> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
const userFields = [
|
||||
'id',
|
||||
'firstName',
|
||||
@ -660,19 +663,20 @@ export class UserResolver {
|
||||
'deletedAt',
|
||||
'userRoles',
|
||||
]
|
||||
const [users, count] = await userRepository.findBySearchCriteriaPagedFiltered(
|
||||
const [users, count] = await findUsers(
|
||||
userFields.map((fieldName) => {
|
||||
return 'user.' + fieldName
|
||||
}),
|
||||
searchText,
|
||||
query,
|
||||
filters ?? null,
|
||||
currentPage,
|
||||
pageSize,
|
||||
order,
|
||||
)
|
||||
|
||||
if (users.length === 0) {
|
||||
return {
|
||||
userCount: 0,
|
||||
userCount: count,
|
||||
userList: [],
|
||||
}
|
||||
}
|
||||
@ -777,7 +781,7 @@ export class UserResolver {
|
||||
@Arg('userId', () => Int) userId: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Date | null> {
|
||||
const user = await DbUser.findOne({ id: userId })
|
||||
const user = await DbUser.findOne({ where: { id: userId } })
|
||||
// user exists ?
|
||||
if (!user) {
|
||||
throw new LogError('Could not find user with given ID', userId)
|
||||
@ -790,7 +794,7 @@ export class UserResolver {
|
||||
// soft-delete user
|
||||
await user.softRemove()
|
||||
await EVENT_ADMIN_USER_DELETE(user, moderator)
|
||||
const newUser = await DbUser.findOne({ id: userId }, { withDeleted: true })
|
||||
const newUser = await DbUser.findOne({ where: { id: userId }, withDeleted: true })
|
||||
return newUser ? newUser.deletedAt : null
|
||||
}
|
||||
|
||||
@ -800,7 +804,7 @@ export class UserResolver {
|
||||
@Arg('userId', () => Int) userId: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Date | null> {
|
||||
const user = await DbUser.findOne({ id: userId }, { withDeleted: true })
|
||||
const user = await DbUser.findOne({ where: { id: userId }, withDeleted: true })
|
||||
if (!user) {
|
||||
throw new LogError('Could not find user with given ID', userId)
|
||||
}
|
||||
@ -852,10 +856,11 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
const dbUserContact = await DbUserContact.findOneOrFail(
|
||||
{ email },
|
||||
{ withDeleted: true, relations: ['user'] },
|
||||
).catch(() => {
|
||||
const dbUserContact = await DbUserContact.findOneOrFail({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
}).catch(() => {
|
||||
throw new LogError('No user with this credentials', email)
|
||||
})
|
||||
const dbUser = dbUserContact.user
|
||||
@ -865,7 +870,10 @@ export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
}
|
||||
|
||||
async function checkEmailExists(email: string): Promise<boolean> {
|
||||
const userContact = await DbUserContact.findOne({ email }, { withDeleted: true })
|
||||
const userContact = await DbUserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
})
|
||||
if (userContact) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import { Connection } from '@dbTools/typeorm'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
|
||||
|
||||
@ -219,7 +220,7 @@ describe('semaphore', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not throw, but should', async () => {
|
||||
it('does throw error on second redeem call', async () => {
|
||||
const redeem1 = mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
@ -236,7 +237,7 @@ describe('semaphore', () => {
|
||||
errors: undefined,
|
||||
})
|
||||
await expect(redeem2).resolves.toMatchObject({
|
||||
errors: undefined,
|
||||
errors: [new GraphQLError('Transaction link already redeemed')],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
import { In } from '@dbTools/typeorm'
|
||||
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||
|
||||
import { ContributionMessageType } from '@enum/ContributionMessageType'
|
||||
import { Order } from '@enum/Order'
|
||||
|
||||
interface FindContributionMessagesOptions {
|
||||
contributionId: number
|
||||
pageSize: number
|
||||
currentPage: number
|
||||
order: Order
|
||||
showModeratorType?: boolean
|
||||
}
|
||||
|
||||
export const findContributionMessages = async (
|
||||
options: FindContributionMessagesOptions,
|
||||
): Promise<[DbContributionMessage[], number]> => {
|
||||
const { contributionId, pageSize, currentPage, order, showModeratorType } = options
|
||||
|
||||
const messageTypes = [ContributionMessageType.DIALOG, ContributionMessageType.HISTORY]
|
||||
|
||||
if (showModeratorType) messageTypes.push(ContributionMessageType.MODERATOR)
|
||||
|
||||
return DbContributionMessage.findAndCount({
|
||||
where: {
|
||||
contributionId,
|
||||
type: In(messageTypes),
|
||||
},
|
||||
relations: ['user'],
|
||||
order: {
|
||||
createdAt: order,
|
||||
},
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
}
|
||||
@ -1,38 +1,73 @@
|
||||
import { In } from '@dbTools/typeorm'
|
||||
import { In, Like } from '@dbTools/typeorm'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
|
||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { Order } from '@enum/Order'
|
||||
|
||||
interface Relations {
|
||||
[key: string]: boolean | Relations
|
||||
}
|
||||
|
||||
interface FindContributionsOptions {
|
||||
order: Order
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
withDeleted?: boolean
|
||||
relations?: string[]
|
||||
relations?: Relations | undefined
|
||||
userId?: number | null
|
||||
statusFilter?: ContributionStatus[] | null
|
||||
query?: string | null
|
||||
}
|
||||
|
||||
export const findContributions = async (
|
||||
options: FindContributionsOptions,
|
||||
): Promise<[DbContribution[], number]> => {
|
||||
const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter } = {
|
||||
const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter, query } = {
|
||||
withDeleted: false,
|
||||
relations: [],
|
||||
relations: undefined,
|
||||
query: '',
|
||||
...options,
|
||||
}
|
||||
|
||||
const requiredWhere = {
|
||||
...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
|
||||
...(userId && { userId }),
|
||||
}
|
||||
|
||||
const where =
|
||||
query && relations?.user
|
||||
? [
|
||||
{
|
||||
...requiredWhere,
|
||||
user: {
|
||||
firstName: Like(`%${query}%`),
|
||||
},
|
||||
},
|
||||
{
|
||||
...requiredWhere,
|
||||
user: {
|
||||
lastName: Like(`%${query}%`),
|
||||
},
|
||||
},
|
||||
{
|
||||
...requiredWhere,
|
||||
user: {
|
||||
emailContact: {
|
||||
email: Like(`%${query}%`),
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: requiredWhere
|
||||
|
||||
return DbContribution.findAndCount({
|
||||
where: {
|
||||
...(statusFilter?.length && { contributionStatus: In(statusFilter) }),
|
||||
...(userId && { userId }),
|
||||
},
|
||||
relations,
|
||||
where,
|
||||
withDeleted,
|
||||
order: {
|
||||
createdAt: order,
|
||||
id: order,
|
||||
},
|
||||
relations,
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
|
||||
@ -7,20 +7,20 @@ import { LogError } from '@/server/LogError'
|
||||
import { VALID_ALIAS_REGEX } from './validateAlias'
|
||||
|
||||
export const findUserByIdentifier = async (identifier: string): Promise<DbUser> => {
|
||||
let user: DbUser | undefined
|
||||
let user: DbUser | null
|
||||
if (validate(identifier) && version(identifier) === 4) {
|
||||
user = await DbUser.findOne({ where: { gradidoID: identifier }, relations: ['emailContact'] })
|
||||
if (!user) {
|
||||
throw new LogError('No user found to given identifier', identifier)
|
||||
}
|
||||
} else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) {
|
||||
const userContact = await DbUserContact.findOne(
|
||||
{
|
||||
const userContact = await DbUserContact.findOne({
|
||||
where: {
|
||||
email: identifier,
|
||||
emailChecked: true,
|
||||
},
|
||||
{ relations: ['user'] },
|
||||
)
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!userContact) {
|
||||
throw new LogError('No user with this credentials', identifier)
|
||||
}
|
||||
|
||||
57
backend/src/graphql/resolver/util/findUsers.ts
Normal file
57
backend/src/graphql/resolver/util/findUsers.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { getConnection, Brackets, IsNull, Not } from '@dbTools/typeorm'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
|
||||
import { Order } from '@enum/Order'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
export const findUsers = async (
|
||||
select: string[],
|
||||
searchCriteria: string,
|
||||
filters: SearchUsersFilters | null,
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
order = Order.ASC,
|
||||
): Promise<[DbUser[], number]> => {
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
try {
|
||||
await queryRunner.connect()
|
||||
const query = queryRunner.manager
|
||||
.createQueryBuilder(DbUser, 'user')
|
||||
.select(select)
|
||||
.withDeleted()
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.where(
|
||||
new Brackets((qb) => {
|
||||
qb.where(
|
||||
'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email',
|
||||
{
|
||||
name: `%${searchCriteria}%`,
|
||||
lastName: `%${searchCriteria}%`,
|
||||
email: `%${searchCriteria}%`,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
if (filters) {
|
||||
if (filters.byActivated !== null) {
|
||||
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
|
||||
}
|
||||
|
||||
if (filters.byDeleted !== null) {
|
||||
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||
}
|
||||
}
|
||||
|
||||
return await query
|
||||
.orderBy({ 'user.id': order })
|
||||
.take(pageSize)
|
||||
.skip((currentPage - 1) * pageSize)
|
||||
.getManyAndCount()
|
||||
} catch (err) {
|
||||
throw new LogError('Unable to search users', err)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
}
|
||||
19
backend/src/graphql/resolver/util/getKlicktippState.ts
Normal file
19
backend/src/graphql/resolver/util/getKlicktippState.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { KlickTipp } from '@model/KlickTipp'
|
||||
|
||||
import { getKlickTippUser } from '@/apis/KlicktippController'
|
||||
import { klickTippLogger as logger } from '@/server/logger'
|
||||
|
||||
export const getKlicktippState = async (email: string): Promise<KlickTipp> => {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const klickTippUser = await getKlickTippUser(email)
|
||||
if (klickTippUser) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
return new KlickTipp(klickTippUser.status === 'Subscribed')
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('There is no klicktipp user for email', email, err)
|
||||
}
|
||||
return new KlickTipp(false)
|
||||
}
|
||||
@ -3,12 +3,10 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
export const getLastTransaction = async (
|
||||
userId: number,
|
||||
relations?: string[],
|
||||
): Promise<DbTransaction | undefined> => {
|
||||
return DbTransaction.findOne(
|
||||
{ userId },
|
||||
{
|
||||
order: { balanceDate: 'DESC', id: 'DESC' },
|
||||
relations,
|
||||
},
|
||||
)
|
||||
): Promise<DbTransaction | null> => {
|
||||
return DbTransaction.findOne({
|
||||
where: { userId },
|
||||
order: { balanceDate: 'DESC', id: 'DESC' },
|
||||
relations,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { MoreThan } from '@dbTools/typeorm'
|
||||
import { MoreThan, IsNull } from '@dbTools/typeorm'
|
||||
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
@ -22,7 +22,7 @@ export async function transactionLinkList(
|
||||
const [transactionLinks, count] = await DbTransactionLink.findAndCount({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(!withRedeemed && { redeemedBy: null }),
|
||||
...(!withRedeemed && { redeemedBy: IsNull() }),
|
||||
...(!withExpired && { validUntil: MoreThan(new Date()) }),
|
||||
},
|
||||
withDeleted,
|
||||
|
||||
49
backend/src/graphql/resolver/util/transactionLinkSummary.ts
Normal file
49
backend/src/graphql/resolver/util/transactionLinkSummary.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { getConnection } from '@dbTools/typeorm'
|
||||
import { TransactionLink as DbTransactionLink } from '@entity/TransactionLink'
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
export const transactionLinkSummary = async (
|
||||
userId: number,
|
||||
date: Date,
|
||||
): Promise<{
|
||||
sumHoldAvailableAmount: Decimal
|
||||
sumAmount: Decimal
|
||||
lastDate: Date | null
|
||||
firstDate: Date | null
|
||||
transactionLinkcount: number
|
||||
}> => {
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
try {
|
||||
await queryRunner.connect()
|
||||
const { sumHoldAvailableAmount, sumAmount, lastDate, firstDate, count } =
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder(DbTransactionLink, 'transactionLink')
|
||||
.select('SUM(transactionLink.holdAvailableAmount)', 'sumHoldAvailableAmount')
|
||||
.addSelect('SUM(transactionLink.amount)', 'sumAmount')
|
||||
.addSelect('MAX(transactionLink.validUntil)', 'lastDate')
|
||||
.addSelect('MIN(transactionLink.createdAt)', 'firstDate')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('transactionLink.userId = :userId', { userId })
|
||||
.andWhere('transactionLink.redeemedAt is NULL')
|
||||
.andWhere('transactionLink.validUntil > :date', { date })
|
||||
.orderBy('transactionLink.createdAt', 'DESC')
|
||||
.getRawOne()
|
||||
return {
|
||||
sumHoldAvailableAmount: sumHoldAvailableAmount
|
||||
? new Decimal(sumHoldAvailableAmount)
|
||||
: new Decimal(0),
|
||||
sumAmount: sumAmount ? new Decimal(sumAmount) : new Decimal(0),
|
||||
lastDate: lastDate || null,
|
||||
firstDate: firstDate || null,
|
||||
transactionLinkcount: count || 0,
|
||||
}
|
||||
} catch (err) {
|
||||
throw new LogError('Unable to get transaction link summary', err)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
}
|
||||
@ -95,7 +95,7 @@ describe('validate alias', () => {
|
||||
describe('test against existing alias in database', () => {
|
||||
beforeAll(async () => {
|
||||
const bibi = await userFactory(testEnv, bibiBloxberg)
|
||||
const user = await User.findOne({ id: bibi.id })
|
||||
const user = await User.findOne({ where: { id: bibi.id } })
|
||||
if (user) {
|
||||
user.alias = 'b-b'
|
||||
await user.save()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user