Merge branch 'master' into cypress-reporting

This commit is contained in:
mahula 2023-07-05 10:29:10 +02:00 committed by GitHub
commit 672b2e5fb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
230 changed files with 8353 additions and 3556 deletions

View File

@ -4,8 +4,78 @@ 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.1](https://github.com/gradido/gradido/compare/1.22.0...1.22.1)
- fix(backend): use base url from config in email templates [`#3114`](https://github.com/gradido/gradido/pull/3114)
- feat(frontend): test right side layout template [`#3052`](https://github.com/gradido/gradido/pull/3052)
- feat(backend): remove iota from backend [`#3115`](https://github.com/gradido/gradido/pull/3115)
- refactor(frontend): text juni to juli [`#3116`](https://github.com/gradido/gradido/pull/3116)
- refactor(frontend): refactor changes incorporated [`#3113`](https://github.com/gradido/gradido/pull/3113)
- refactor(frontend): date from deploy changed to infotext [`#3108`](https://github.com/gradido/gradido/pull/3108)
- fix(frontend): add alias to the verifyLogin GraphQL answer. [`#3107`](https://github.com/gradido/gradido/pull/3107)
- fix(backend): moderator message don't send email to user [`#3106`](https://github.com/gradido/gradido/pull/3106)
#### [1.22.0](https://github.com/gradido/gradido/compare/1.21.0...1.22.0)
> 30 June 2023
- chore(release): v1.22.0 [`#3101`](https://github.com/gradido/gradido/pull/3101)
- fix(backend): yarn.lock after typeorm update [`#3097`](https://github.com/gradido/gradido/pull/3097)
- feat(frontend): new style for settings page [`#3040`](https://github.com/gradido/gradido/pull/3040)
- feat(admin): query users on contributions [`#3094`](https://github.com/gradido/gradido/pull/3094)
- feat(backend): user query on find contributions [`#3091`](https://github.com/gradido/gradido/pull/3091)
- fix(backend): double redeem transaction link [`#3093`](https://github.com/gradido/gradido/pull/3093)
- feat(admin): message type admin frontend [`#3073`](https://github.com/gradido/gradido/pull/3073)
- feat(other): end-to-end test scenarios for deleted and not registered user [`#3077`](https://github.com/gradido/gradido/pull/3077)
- feat(database): update typeorm [`#3078`](https://github.com/gradido/gradido/pull/3078)
- refactor(other): disable cypress test retries [`#3092`](https://github.com/gradido/gradido/pull/3092)
- test(other): update cypress [`#3056`](https://github.com/gradido/gradido/pull/3056)
- feat(other): add definition of cron-job for klicktipp export. [`#3051`](https://github.com/gradido/gradido/pull/3051)
- fix(backend): forget password not for deleted users [`#3090`](https://github.com/gradido/gradido/pull/3090)
- fix(backend): gdt server error & gdt refetch policy [`#3086`](https://github.com/gradido/gradido/pull/3086)
- feat(backend): contribution message type moderator [`#3072`](https://github.com/gradido/gradido/pull/3072)
- fix(other): linting in e2e tests directory does not work [`#3089`](https://github.com/gradido/gradido/pull/3089)
- feat(other): end-to-end test feature send coins [`#3070`](https://github.com/gradido/gradido/pull/3070)
- refactor(dht): move typescript related packages in dev Dependencies [`#3085`](https://github.com/gradido/gradido/pull/3085)
- fix(backend): jest environment [`#3084`](https://github.com/gradido/gradido/pull/3084)
- feat(frontend): transaction list page change style for small device [`#3081`](https://github.com/gradido/gradido/pull/3081)
- refactor(other): refactor state to status [`#3082`](https://github.com/gradido/gradido/pull/3082)
- feat(admin): change deny contribution btn-warning color and background-color to #e1a908 [`#3080`](https://github.com/gradido/gradido/pull/3080)
- feat(backend): bootstrap and Hello World Test [`#3041`](https://github.com/gradido/gradido/pull/3041)
- feat(frontend): change the info text on the start page [`#3049`](https://github.com/gradido/gradido/pull/3049)
- refactor(dht): eslint dht n [`#3045`](https://github.com/gradido/gradido/pull/3045)
- refactor(dht): eslint dht comments [`#3046`](https://github.com/gradido/gradido/pull/3046)
- fix(frontend): auto logout messages autohide time 5000 [`#2959`](https://github.com/gradido/gradido/pull/2959)
- feat(federation): introduce private key in community table [`#3024`](https://github.com/gradido/gradido/pull/3024)
- refactor(backend): removed klicktipp middleware and replace it with a function [`#3035`](https://github.com/gradido/gradido/pull/3035)
- feat(admin): order user search desc, fix pagination [`#3059`](https://github.com/gradido/gradido/pull/3059)
- refactor(database): eslint database eslint comments [`#3039`](https://github.com/gradido/gradido/pull/3039)
- refactor(database): eslint database import [`#3038`](https://github.com/gradido/gradido/pull/3038)
- feat(backend): apply design template to HTML e-mails [`#2938`](https://github.com/gradido/gradido/pull/2938)
- refactor(database): eslint database n [`#3037`](https://github.com/gradido/gradido/pull/3037)
- refactor(backend): sodium native with types [`#3032`](https://github.com/gradido/gradido/pull/3032)
- refactor(federation): expose private key to write home community [`#3023`](https://github.com/gradido/gradido/pull/3023)
- refactor(dht): eslint dht base configuration [`#3042`](https://github.com/gradido/gradido/pull/3042)
- refactor(federation): eslint federation base configuration [`#3047`](https://github.com/gradido/gradido/pull/3047)
- refactor(database): eslint database base configuration [`#3036`](https://github.com/gradido/gradido/pull/3036)
- feat(workflow): dht as package for lint pr workflow [`#3043`](https://github.com/gradido/gradido/pull/3043)
- refactor(federation): removed (potential) duplicate db query [`#3019`](https://github.com/gradido/gradido/pull/3019)
- refactor(federation): remove function getSeed [`#3022`](https://github.com/gradido/gradido/pull/3022)
- refactor(federation): refactor federation to use inheritance [`#2992`](https://github.com/gradido/gradido/pull/2992)
- refactor(backend): random-bigint types [`#3033`](https://github.com/gradido/gradido/pull/3033)
- fix(frontend): incorrect errormessage for wrong contribution link [`#2958`](https://github.com/gradido/gradido/pull/2958)
- refactor(federation): remove export from CommunityApi [`#3021`](https://github.com/gradido/gradido/pull/3021)
- refactor(federation): simplify newCommunityUuid [`#3020`](https://github.com/gradido/gradido/pull/3020)
- refactor(database): remove duplicate comments [`#3025`](https://github.com/gradido/gradido/pull/3025)
- feat(federation): x com 3 introduce business communities [`#2955`](https://github.com/gradido/gradido/pull/2955)
- refactor(backend): remove to do after creating issue [`#3000`](https://github.com/gradido/gradido/pull/3000)
- refactor(backend): camelcase exception for FederationClient_XX_X [`#3016`](https://github.com/gradido/gradido/pull/3016)
#### [1.21.0](https://github.com/gradido/gradido/compare/1.20.0...1.21.0)
> 19 May 2023
- chore(release): v1.21.0 [`#2998`](https://github.com/gradido/gradido/pull/2998)
- feat(frontend): preserve email after login [`#2994`](https://github.com/gradido/gradido/pull/2994)
- feat(frontend): send coins via identifier [`#2989`](https://github.com/gradido/gradido/pull/2989)
- feat(backend): export user events to klicktipp [`#2916`](https://github.com/gradido/gradido/pull/2916)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,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,

View File

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

View File

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

View File

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

View File

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

View File

@ -53,4 +53,5 @@ 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',
}

View File

@ -12,14 +12,14 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0066-x-community-sendcoins-transactions_table',
DB_VERSION: '0067-private_key_in_community_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: 'v17.2023-07-03',
CURRENT: '',
},
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -70,7 +70,36 @@ export const sendEmailTranslated = async ({
const resultSend = await email
.send({
template: path.join(__dirname, 'templates', template),
message: receiver,
message: {
...receiver,
attachments: [
{
filename: 'gradido-header.jpeg',
path: path.join(__dirname, 'templates/includes/gradido-header.jpeg'),
cid: 'gradidoheader',
},
{
filename: 'facebook-icon.png',
path: path.join(__dirname, 'templates/includes/facebook-icon.png'),
cid: 'facebookicon',
},
{
filename: 'telegram-icon.png',
path: path.join(__dirname, 'templates/includes/telegram-icon.png'),
cid: 'telegramicon',
},
{
filename: 'twitter-icon.png',
path: path.join(__dirname, 'templates/includes/twitter-icon.png'),
cid: 'twittericon',
},
{
filename: 'youtube-icon.png',
path: path.join(__dirname, 'templates/includes/youtube-icon.png'),
cid: 'youtubeicon',
},
],
},
locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
})
.catch((error: unknown) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,41 @@ describe('ContributionMessageResolver', () => {
)
})
})
describe('contribution message type MODERATOR', () => {
beforeAll(() => {
jest.clearAllMocks()
})
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',
}),
},
}),
)
})
it("don't call sendAddedContributionMessageEmail", () => {
expect(sendAddedContributionMessageEmail).not.toBeCalled()
})
})
})
})
@ -385,7 +420,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 +454,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',
}),
]),
},
},
}),
)
})
})
})
})

View File

@ -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)
@ -146,15 +167,17 @@ export class ContributionMessageResolver {
await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
}
void sendAddedContributionMessageEmail({
firstName: contribution.user.firstName,
lastName: contribution.user.lastName,
email: contribution.user.emailContact.email,
language: contribution.user.language,
senderFirstName: moderator.firstName,
senderLastName: moderator.lastName,
contributionMemo: contribution.memo,
})
if (messageType !== ContributionMessageType.MODERATOR) {
void sendAddedContributionMessageEmail({
firstName: contribution.user.firstName,
lastName: contribution.user.lastName,
email: contribution.user.emailContact.email,
language: contribution.user.language,
senderFirstName: moderator.firstName,
senderLastName: moderator.lastName,
contributionMemo: contribution.memo,
})
}
await queryRunner.commitTransaction()
await EVENT_ADMIN_CONTRIBUTION_MESSAGE_CREATE(
{ id: contribution.userId } as DbUser,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -195,10 +195,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,
@ -271,10 +273,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,
@ -292,7 +294,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',
@ -334,7 +336,7 @@ describe('UserResolver', () => {
})
// make Peter Lustig Admin
const peter = await User.findOneOrFail({ id: user[0].id })
const peter = await User.findOneOrFail({ where: { id: user[0].id } })
peter.isAdmin = new Date()
await peter.save()
@ -365,7 +367,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({
@ -445,7 +447,7 @@ describe('UserResolver', () => {
memo: `testing transaction link`,
})
transactionLink = await TransactionLink.findOneOrFail()
transactionLink = await TransactionLink.findOneOrFail({ where: { userId: bob.id } })
resetToken()
@ -462,7 +464,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 }),
@ -529,16 +531,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 () => {
@ -571,7 +575,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()
})
@ -697,10 +703,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,
@ -879,10 +885,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,
@ -1047,10 +1053,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,
@ -1083,7 +1089,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 () => {
@ -1100,7 +1106,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"
}
}`),
],
}),
@ -1175,20 +1183,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,
@ -1212,11 +1220,11 @@ describe('UserResolver', () => {
alias: 'bibi_Bloxberg',
},
})
await expect(User.findOne()).resolves.toEqual(
await expect(User.find()).resolves.toEqual([
expect.objectContaining({
alias: 'bibi_Bloxberg',
}),
)
])
})
})
})
@ -1433,10 +1441,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(
@ -1450,10 +1458,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(
@ -1590,14 +1598,14 @@ describe('UserResolver', () => {
})
it('stores the ADMIN_USER_ROLE_SET 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_ROLE_SET,
@ -1792,14 +1800,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,
@ -1943,10 +1952,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(),
@ -1965,10 +1974,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,
@ -2086,14 +2095,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,
@ -2110,7 +2119,7 @@ describe('UserResolver', () => {
describe('search users', () => {
const variablesWithoutTextAndFilters = {
searchText: '',
query: '',
currentPage: 1,
pageSize: 25,
filters: null,

View File

@ -2,28 +2,18 @@
/* 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, IsNull, Not } from '@dbTools/typeorm'
import { getConnection, 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 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'
@ -33,7 +23,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'
@ -60,7 +49,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'
@ -71,15 +59,15 @@ 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'
// 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 => {
@ -94,13 +82,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())
}
@ -122,7 +110,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)
@ -132,12 +119,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,
@ -183,6 +170,7 @@ export class UserResolver {
dbUser.publisherId = publisherId
await DbUser.save(dbUser)
}
user.klickTipp = await getKlicktippState(dbUser.emailContact.email)
context.setHeaders.push({
key: 'token',
@ -207,7 +195,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(
@ -237,12 +233,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({
@ -276,13 +275,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) {
@ -290,7 +292,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
@ -364,7 +366,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
}
@ -377,7 +380,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)
@ -415,10 +418,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...')
@ -486,7 +489,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)) {
@ -613,9 +618,7 @@ export class UserResolver {
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
): Promise<SearchAdminUsersResult> {
const userRepository = getCustomRepository(UserRepository)
const [users, count] = await userRepository.findAndCount({
const [users, count] = await DbUser.findAndCount({
where: {
isAdmin: Not(IsNull()),
},
@ -640,12 +643,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',
@ -655,19 +660,20 @@ export class UserResolver {
'deletedAt',
'isAdmin',
]
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: [],
}
}
@ -713,7 +719,7 @@ export class UserResolver {
@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)
@ -742,7 +748,7 @@ export class UserResolver {
}
await user.save()
await EVENT_ADMIN_USER_ROLE_SET(user, moderator)
const newUser = await DbUser.findOne({ id: userId })
const newUser = await DbUser.findOne({ where: { id: userId } })
return newUser ? newUser.isAdmin : null
}
@ -752,7 +758,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)
@ -765,7 +771,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
}
@ -775,7 +781,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)
}
@ -827,10 +833,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
@ -839,7 +846,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
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,24 @@
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
import { getConnection, Brackets, IsNull, Not } from '@dbTools/typeorm'
import { User as DbUser } from '@entity/User'
import { SearchUsersFilters } from '@/graphql/arg/SearchUsersFilters'
import { SearchUsersFilters } from '@arg/SearchUsersFilters'
import { Order } from '@enum/Order'
@EntityRepository(DbUser)
export class UserRepository extends Repository<DbUser> {
async findBySearchCriteriaPagedFiltered(
select: string[],
searchCriteria: string,
filters: SearchUsersFilters | null,
currentPage: number,
pageSize: number,
): Promise<[DbUser[], number]> {
const query = this.createQueryBuilder('user')
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')
@ -28,26 +34,24 @@ export class UserRepository extends Repository<DbUser> {
)
}),
)
/*
filterCriteria.forEach((filter) => {
query.andWhere(filter)
})
*/
if (filters) {
if (filters.byActivated !== null) {
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
// filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated })
}
if (filters.byDeleted !== null) {
// filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
}
}
return query
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()
}
}

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

View File

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

View File

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

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

View File

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

View File

@ -1,69 +1,92 @@
{
"emails": {
"accountActivation": {
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
"activateAccount": "Konto aktivieren",
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
"pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:",
"subject": "Gradido: E-Mail Überprüfung"
"pleaseClickLink": "Klicke bitte hier, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren.",
"subject": "E-Mail Überprüfung",
"title": "E-Mail Überprüfung"
},
"accountMultiRegistration": {
"contactSupport": "Support kontaktieren",
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.",
"emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
"ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der versucht hat sich erneut zu registrieren, wende dich bitte an unseren Support:",
"onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
"ifYouAreNotTheOne": "Wenn du nicht versucht hast dich erneut zu registrieren, wende dich bitte an unseren Support:",
"onForgottenPasswordClickLink": "Solltest du dein Passwort vergessen haben, klicke bitte hier.",
"subject": "Erneuter Registrierungsversuch mit deiner E-Mail",
"title": "Erneuter Registrierungsversuch mit deiner E-Mail"
},
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“!"
"readMessage": "Nachricht lesen und beantworten",
"subject": "Nachricht zu deinem Gemeinwohl-Beitrag",
"title": "Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“."
},
"contribution": {
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“."
},
"contributionConfirmed": {
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt. Es wurden deinem Gradido-Konto {amountGDD} GDD gutgeschrieben.",
"subject": "Dein Gemeinwohl-Beitrag wurde bestätigt",
"title": "Dein Gemeinwohl-Beitrag wurde bestätigt"
},
"contributionDeleted": {
"commonGoodContributionDeleted": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} gelöscht.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde gelöscht",
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“!"
"subject": "Dein Gemeinwohl-Beitrag wurde gelöscht",
"title": "Dein Gemeinwohl-Beitrag wurde gelöscht"
},
"contributionDenied": {
"commonGoodContributionDenied": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt",
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Schöpfen“ auf den Tab „Meine Beiträge“!"
"subject": "Dein Gemeinwohl-Beitrag wurde abgelehnt",
"title": "Dein Gemeinwohl-Beitrag wurde abgelehnt"
},
"footer": {
"contactOurSupport": "Bei weiteren Fragen kontaktiere bitte unseren Support.",
"imprint": "Impressum",
"imprintAddress": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
"imprintImageAlt": "Gradido-Akademie Logo",
"privacyPolicy": "Datenschutzerklärung",
"supportEmail": "support@gradido.net"
},
"general": {
"amountGDD": "Betrag: {amountGDD} GDD",
"detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:",
"doNotAnswer": "nicht antworten",
"completeRegistration": "Registrierung abschließen",
"contribution": "Gemeinwohl-Beitrag: {contributionMemo}",
"contributionDetails": "Beitragsdetails",
"detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto.",
"helloName": "Hallo {firstName} {lastName},",
"linkToYourAccount": "Link zu deinem Konto:",
"orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!",
"linkValidity": "Der Link hat eine Gültigkeit von {hours} Stunden.\nSollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen.",
"linkValidityWithMinutes": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten.\nSollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen.",
"newLink": "Neuer Link",
"orCopyLink": "Oder kopiere den Link in dein Browserfenster.",
"pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail.",
"requestNewLink": "Neuen gültigen Link anfordern",
"reset": "zurücksetzen",
"sincerelyYours": "Liebe Grüße",
"toAccount": "Zum Konto",
"transactionDetails": "Transaktionsdetails",
"yourGradidoTeam": "dein Gradido-Team"
},
"resetPassword": {
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
"pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:",
"subject": "Gradido: Passwort zurücksetzen",
"youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert."
"pleaseClickLink": "Wenn du es warst, klicke bitte hier.",
"subject": "Passwort zurücksetzen",
"title": "Passwort zurücksetzen",
"youOrSomeoneResetPassword": "du oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert."
},
"transactionLinkRedeemed": {
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.",
"memo": "Nachricht: {transactionMemo}",
"subject": "Gradido: {senderFirstName} {senderLastName} hat deinen Gradido-Link eingelöst"
"subject": "{senderFirstName} {senderLastName} hat deinen Gradido-Link eingelöst",
"title": "{senderFirstName} {senderLastName} hat deinen Gradido-Link eingelöst"
},
"transactionReceived": {
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.",
"subject": "Gradido: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
"subject": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet",
"title": "{senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
}
},
"general": {
"decimalSeparator": ",",
"imprint": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
"imprintImageAlt": "Gradido-Akademie Logo",
"imprintImageURL": "https://gdd.gradido.net/img/brand/green.png"
"decimalSeparator": ","
}
}

View File

@ -1,69 +1,92 @@
{
"emails": {
"accountActivation": {
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
"activateAccount": "Activate account",
"emailRegistered": "Your email address has just been registered with Gradido.",
"pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:",
"subject": "Gradido: Email Verification"
"pleaseClickLink": "Please click here to complete the registration and activate your Gradido account.",
"subject": "Email Verification",
"title": "Email Verification"
},
"accountMultiRegistration": {
"contactSupport": "Contact support",
"emailExists": "However, an account already exists for your email address.",
"emailReused": "Your email address has just been used again to register an account with Gradido.",
"ifYouAreNotTheOne": "If you are not the one who tried to register again, please contact our support:",
"onForgottenPasswordClickLink": "Please click on the following link if you have forgotten your password:",
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
"subject": "Gradido: Try To Register Again With Your Email"
"ifYouAreNotTheOne": "If you did not try to register again, please contact our support:",
"onForgottenPasswordClickLink": "If you have forgotten your password, please click here.",
"subject": "Try To Register Again With Your Email",
"title": "Try To Register Again With Your Email"
},
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!"
"commonGoodContributionMessage": "You have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"readMessage": "Read and reply to message",
"subject": "Message about your common good contribution",
"title": "Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab."
},
"contribution": {
"toSeeContributionsAndMessages": "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."
},
"contributionConfirmed": {
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
"subject": "Gradido: Your contribution to the common good was confirmed"
"commonGoodContributionConfirmed": "Your common good contribution “{contributionMemo}” has just been approved by {senderFirstName} {senderLastName}. Your Gradido account has been credited with {amountGDD} GDD.",
"subject": "Your contribution to the common good was confirmed",
"title": "Your contribution to the common good was confirmed"
},
"contributionDeleted": {
"commonGoodContributionDeleted": "Your public good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was deleted",
"toSeeContributionsAndMessages": "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!"
"commonGoodContributionDeleted": "Your common good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.",
"subject": "Your common good contribution was deleted",
"title": "Your common good contribution was deleted"
},
"contributionDenied": {
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was rejected",
"toSeeContributionsAndMessages": "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!"
"commonGoodContributionDenied": "Your common good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
"subject": "Your common good contribution was rejected",
"title": "Your common good contribution was rejected"
},
"footer": {
"contactOurSupport": "If you have any further questions, please contact our support.",
"imprint": "Impressum",
"imprintAddress": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
"imprintImageAlt": "Gradido-Akademie Logo",
"privacyPolicy": "Privacy Policy",
"supportEmail": "support@gradido.net"
},
"general": {
"amountGDD": "Amount: {amountGDD} GDD",
"detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:",
"doNotAnswer": "do not answer",
"helloName": "Hello {firstName} {lastName}",
"linkToYourAccount": "Link to your account:",
"orCopyLink": "or copy the link above into your browser window.",
"pleaseDoNotReply": "Please do not reply to this email!",
"completeRegistration": "Complete registration",
"contribution": "Contribution: : {contributionMemo}",
"contributionDetails": "Contribution details",
"detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account.",
"helloName": "Hello {firstName} {lastName},",
"linkValidity": "The link has a validity of {hours} hours.\nIf the validity of the link has already expired, you can have a new link sent to you here.",
"linkValidityWithMinutes": "The link has a validity of {hours} hours and {minutes} minutes.\nIf the validity of the link has already expired, you can have a new link sent to you here.",
"newLink": "New link",
"orCopyLink": "Or copy the link into your browser window.",
"pleaseDoNotReply": "Please do not reply to this email.",
"requestNewLink": "Request new valid link",
"reset": "reset",
"sincerelyYours": "Kind regards,",
"toAccount": "To account",
"transactionDetails": "Transaction details",
"yourGradidoTeam": "your Gradido team"
},
"resetPassword": {
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
"pleaseClickLink": "If it was you, please click on the link:",
"subject": "Gradido: Reset password",
"pleaseClickLink": "If it was you, please click here.",
"subject": "Reset password",
"title": "Reset password",
"youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account."
},
"transactionLinkRedeemed": {
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.",
"memo": "Message: {transactionMemo}",
"subject": "Gradido: {senderFirstName} {senderLastName} has redeemed your Gradido link"
"subject": "{senderFirstName} {senderLastName} has redeemed your Gradido link",
"title": "{senderFirstName} {senderLastName} has redeemed your Gradido link"
},
"transactionReceived": {
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).",
"subject": "Gradido: {senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido"
"subject": "{senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido",
"title": "{senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido"
}
},
"general": {
"decimalSeparator": ".",
"imprint": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
"imprintImageAlt": "Gradido-Akademie Logo",
"imprintImageURL": "https://gdd.gradido.net/img/brand/green.png"
"decimalSeparator": "."
}
}

View File

@ -1,31 +0,0 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { MiddlewareFn } from 'type-graphql'
import { KlickTipp } from '@model/KlickTipp'
import { getKlickTippUser } from '@/apis/KlicktippController'
import { klickTippLogger as logger } from '@/server/logger'
export const klicktippNewsletterStateMiddleware: MiddlewareFn = async (
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
{ root, args, context, info },
next,
) => {
// eslint-disable-next-line n/callback-return
const result = await next()
let klickTipp = new KlickTipp({ status: 'Unsubscribed' })
try {
const klickTippUser = await getKlickTippUser(result.email)
if (klickTippUser) {
klickTipp = new KlickTipp(klickTippUser)
}
} catch (err) {
logger.error(`There is no user for (email='${result.email}') ${err}`)
}
result.klickTipp = klickTipp
return result
}

View File

@ -10,8 +10,19 @@ import { CONFIG } from '@/config'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
// eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs
const sodium = require('sodium-native')
import {
crypto_shorthash_KEYBYTES,
crypto_box_SEEDBYTES,
crypto_hash_sha512_init,
crypto_hash_sha512_update,
crypto_hash_sha512_final,
crypto_hash_sha512_BYTES,
crypto_hash_sha512_STATEBYTES,
crypto_shorthash_BYTES,
crypto_pwhash_SALTBYTES,
crypto_pwhash,
crypto_shorthash,
} from 'sodium-native'
// We will reuse this for changePassword
export const isValidPassword = (password: string): boolean => {
@ -22,36 +33,36 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string):
logger.trace('SecretKeyCryptographyCreateKey...')
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
if (configLoginServerKey.length !== crypto_shorthash_KEYBYTES) {
throw new LogError(
'ServerKey has an invalid size',
configLoginServerKey.length,
sodium.crypto_shorthash_KEYBYTES,
crypto_shorthash_KEYBYTES,
)
}
const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
sodium.crypto_hash_sha512_init(state)
sodium.crypto_hash_sha512_update(state, Buffer.from(salt))
sodium.crypto_hash_sha512_update(state, configLoginAppSecret)
const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
sodium.crypto_hash_sha512_final(state, hash)
const state = Buffer.alloc(crypto_hash_sha512_STATEBYTES)
crypto_hash_sha512_init(state)
crypto_hash_sha512_update(state, Buffer.from(salt))
crypto_hash_sha512_update(state, configLoginAppSecret)
const hash = Buffer.alloc(crypto_hash_sha512_BYTES)
crypto_hash_sha512_final(state, hash)
const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES)
const encryptionKey = Buffer.alloc(crypto_box_SEEDBYTES)
const opsLimit = 10
const memLimit = 33554432
const algo = 2
sodium.crypto_pwhash(
crypto_pwhash(
encryptionKey,
Buffer.from(password),
hash.slice(0, sodium.crypto_pwhash_SALTBYTES),
hash.slice(0, crypto_pwhash_SALTBYTES),
opsLimit,
memLimit,
algo,
)
const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
const encryptionKeyHash = Buffer.alloc(crypto_shorthash_BYTES)
crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
return [encryptionKeyHash, encryptionKey]
}

View File

@ -19,7 +19,10 @@ export const creationFactory = async (
creation: CreationInterface,
): Promise<Contribution> => {
const { mutate } = client
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
await mutate({
mutation: login,
variables: { email: creation.email, password: 'Aa12345_' },
})
const {
data: { createContribution: contribution },
@ -30,7 +33,9 @@ export const creationFactory = async (
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
await mutate({ mutation: confirmContribution, variables: { id: contribution.id } })
const confirmedContribution = await Contribution.findOneOrFail({ id: contribution.id })
const confirmedContribution = await Contribution.findOneOrFail({
where: { id: contribution.id },
})
if (creation.moveCreationDate) {
const transaction = await Transaction.findOneOrFail({

View File

@ -32,7 +32,7 @@ export const transactionLinkFactory = async (
} = await mutate({ mutation: createTransactionLink, variables })
if (transactionLink.createdAt || transactionLink.deletedAt) {
const dbTransactionLink = await TransactionLink.findOneOrFail({ id })
const dbTransactionLink = await TransactionLink.findOneOrFail({ where: { id } })
if (transactionLink.createdAt) {
dbTransactionLink.createdAt = transactionLink.createdAt

View File

@ -19,7 +19,7 @@ export const userFactory = async (
} = await mutate({ mutation: createUser, variables: user })
// console.log('creatUser:', { id }, { user })
// get user from database
let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact'] })
let dbUser = await User.findOneOrFail({ where: { id }, relations: ['emailContact'] })
// console.log('dbUser:', dbUser)
const emailContact = dbUser.emailContact
@ -33,7 +33,7 @@ export const userFactory = async (
}
// get last changes of user from database
dbUser = await User.findOneOrFail({ id })
dbUser = await User.findOneOrFail({ where: { id } })
if (user.createdAt || user.deletedAt || user.isAdmin) {
if (user.createdAt) dbUser.createdAt = user.createdAt

View File

@ -50,6 +50,7 @@ export const updateUserInfos = gql`
export const createUser = gql`
mutation (
$alias: String
$firstName: String!
$lastName: String!
$email: String!
@ -58,6 +59,7 @@ export const createUser = gql`
$redeemCode: String
) {
createUser(
alias: $alias
email: $email
firstName: $firstName
lastName: $lastName
@ -284,8 +286,12 @@ export const createContributionMessage = gql`
`
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

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