mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2202-refactor-creations-query
This commit is contained in:
commit
d1a1059158
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -528,7 +528,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 68
|
||||
min_coverage: 74
|
||||
token: ${{ github.token }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
46
CHANGELOG.md
46
CHANGELOG.md
@ -4,8 +4,54 @@ 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.13.1](https://github.com/gradido/gradido/compare/1.13.0...1.13.1)
|
||||
|
||||
- Fix: correctly evaluate to EMAIL_TEST_MODE to false [`#2273`](https://github.com/gradido/gradido/pull/2273)
|
||||
- Refactor: Contribution resolver logs and events [`#2231`](https://github.com/gradido/gradido/pull/2231)
|
||||
|
||||
#### [1.13.0](https://github.com/gradido/gradido/compare/1.12.1...1.13.0)
|
||||
|
||||
> 18 October 2022
|
||||
|
||||
- release: Version 1.13.0 [`#2269`](https://github.com/gradido/gradido/pull/2269)
|
||||
- fix: Linked User Email in Transaction List [`#2268`](https://github.com/gradido/gradido/pull/2268)
|
||||
- concept capturing alias [`#2148`](https://github.com/gradido/gradido/pull/2148)
|
||||
- fix: 🍰 Daily Redeem Of Contribution Link [`#2265`](https://github.com/gradido/gradido/pull/2265)
|
||||
- fix: 🐛 Prevent Loosing Redeem Code When Changing Between Register and Login in Auth Navbar [`#2260`](https://github.com/gradido/gradido/pull/2260)
|
||||
- fix: Disable Change of Month on Update Contribution [`#2264`](https://github.com/gradido/gradido/pull/2264)
|
||||
- feat: 🍰 Global Jest Extension For Decimal Equal [`#2261`](https://github.com/gradido/gradido/pull/2261)
|
||||
- feat: 🍰 Daily Rule For Contribution Links In Admin Interface [`#2262`](https://github.com/gradido/gradido/pull/2262)
|
||||
- feat: 🍰 Do Not Show Expired Contribution Links In Wallet [`#2257`](https://github.com/gradido/gradido/pull/2257)
|
||||
- fix: 🍰 Disable Change Of Month For Update Contribution (wallet and admin) [`#2258`](https://github.com/gradido/gradido/pull/2258)
|
||||
- refactor: 🍰 Login And Logout To Mutations [`#2232`](https://github.com/gradido/gradido/pull/2232)
|
||||
- fix: 🐛 Verify Token Before Redeeming A Link [`#2254`](https://github.com/gradido/gradido/pull/2254)
|
||||
- Refactor: Add all events to documentation table [`#2240`](https://github.com/gradido/gradido/pull/2240)
|
||||
- reconfig log4js with rollover feature and userid in logevent-message [`#2221`](https://github.com/gradido/gradido/pull/2221)
|
||||
- refactor: 🍰 Refactoring Components Of `CotributionMessagesListItem` [`#2251`](https://github.com/gradido/gradido/pull/2251)
|
||||
- style: add border-radius on send form [`#2233`](https://github.com/gradido/gradido/pull/2233)
|
||||
- 2198 adminarea more dates on created transaction [`#2212`](https://github.com/gradido/gradido/pull/2212)
|
||||
- Bug: delete contribution link [`#2213`](https://github.com/gradido/gradido/pull/2213)
|
||||
- chore: 🍰 Fix Cypress Tests Unreliability [`#2245`](https://github.com/gradido/gradido/pull/2245)
|
||||
- docs: 🍰 Refine Deployment Documentation [`#2209`](https://github.com/gradido/gradido/pull/2209)
|
||||
- End-to-end test setup [`#2047`](https://github.com/gradido/gradido/pull/2047)
|
||||
- config testmodus flag for sending emails to test or team account instead of user account [`#2216`](https://github.com/gradido/gradido/pull/2216)
|
||||
- GradidoID 1: adapt and migrate database schema [`#2058`](https://github.com/gradido/gradido/pull/2058)
|
||||
- feat: Add Client Request Time to Context [`#2206`](https://github.com/gradido/gradido/pull/2206)
|
||||
- 2219 feature rework eventprotocol [`#2234`](https://github.com/gradido/gradido/pull/2234)
|
||||
- Refactor: Test register with redeem code [`#2214`](https://github.com/gradido/gradido/pull/2214)
|
||||
- 2203 delete query modal when redeeming the redeem link [`#2211`](https://github.com/gradido/gradido/pull/2211)
|
||||
- Refactor: 🍰 Change email templates [`#2228`](https://github.com/gradido/gradido/pull/2228)
|
||||
- Refactor: Events and logs completed in User Resolver [`#2204`](https://github.com/gradido/gradido/pull/2204)
|
||||
- change support mail [`#2210`](https://github.com/gradido/gradido/pull/2210)
|
||||
- feat: 🍰 Send email when contribution is confirmed [`#2193`](https://github.com/gradido/gradido/pull/2193)
|
||||
- feat: 🍰 Send email when admin writes message to contribution [`#2187`](https://github.com/gradido/gradido/pull/2187)
|
||||
- feat: 🍰 Send Email To Transaction Link Sender After Receiver Redeemed It [`#2063`](https://github.com/gradido/gradido/pull/2063)
|
||||
|
||||
#### [1.12.1](https://github.com/gradido/gradido/compare/1.12.0...1.12.1)
|
||||
|
||||
> 13 September 2022
|
||||
|
||||
- release: Version 1.12.1 [`#2196`](https://github.com/gradido/gradido/pull/2196)
|
||||
- fix: 🍰 Show Not Icons In `allContribution` List [`#2195`](https://github.com/gradido/gradido/pull/2195)
|
||||
|
||||
#### [1.12.0](https://github.com/gradido/gradido/compare/1.11.0...1.12.0)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.12.1",
|
||||
"version": "1.13.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -5,6 +5,7 @@ const localVue = global.localVue
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
|
||||
@ -15,7 +15,10 @@
|
||||
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
||||
<b-card>
|
||||
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
|
||||
<contribution-link-form :contributionLinkData="contributionLinkData" />
|
||||
<contribution-link-form
|
||||
:contributionLinkData="contributionLinkData"
|
||||
@get-contribution-links="$emit('get-contribution-links')"
|
||||
/>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
|
||||
@ -24,6 +27,7 @@
|
||||
v-if="count > 0"
|
||||
:items="items"
|
||||
@editContributionLinkData="editContributionLinkData"
|
||||
@get-contribution-links="$emit('get-contribution-links')"
|
||||
/>
|
||||
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
||||
</b-card-text>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinkForm from './ContributionLinkForm.vue'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
import { createContributionLink } from '@/graphql/createContributionLink.js'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -72,48 +73,70 @@ describe('ContributionLinkForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// describe('successfull submit', () => {
|
||||
// beforeEach(async () => {
|
||||
// mockAPIcall.mockResolvedValue({
|
||||
// data: {
|
||||
// createContributionLink: {
|
||||
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
// await wrapper.find('input.test-validFrom').setValue('2022-6-18')
|
||||
// await wrapper.find('input.test-validTo').setValue('2022-7-18')
|
||||
// await wrapper.find('input.test-name').setValue('test name')
|
||||
// await wrapper.find('input.test-memo').setValue('test memo')
|
||||
// await wrapper.find('input.test-amount').setValue('100')
|
||||
// await wrapper.find('form').trigger('submit')
|
||||
// })
|
||||
describe('successfull submit', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockResolvedValue({
|
||||
data: {
|
||||
createContributionLink: {
|
||||
link: 'https://localhost/redeem/CL-1a2345678',
|
||||
},
|
||||
},
|
||||
})
|
||||
await wrapper
|
||||
.findAllComponents({ name: 'BFormDatepicker' })
|
||||
.at(0)
|
||||
.vm.$emit('input', '2022-6-18')
|
||||
await wrapper
|
||||
.findAllComponents({ name: 'BFormDatepicker' })
|
||||
.at(1)
|
||||
.vm.$emit('input', '2022-7-18')
|
||||
await wrapper.find('input.test-name').setValue('test name')
|
||||
await wrapper.find('textarea.test-memo').setValue('test memo')
|
||||
await wrapper.find('input.test-amount').setValue('100')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
// it('calls the API', () => {
|
||||
// expect(mockAPIcall).toHaveBeenCalledWith(
|
||||
// expect.objectContaining({
|
||||
// variables: {
|
||||
// link: 'https://localhost/redeem/CL-1a2345678',
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// })
|
||||
it('calls the API', () => {
|
||||
expect(apolloMutateMock).toHaveBeenCalledWith({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
validFrom: '2022-6-18',
|
||||
validTo: '2022-7-18',
|
||||
name: 'test name',
|
||||
amount: '100',
|
||||
memo: 'test memo',
|
||||
cycle: 'ONCE',
|
||||
maxPerCycle: 1,
|
||||
maxAmountPerMonth: '0',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// it('displays the new username', () => {
|
||||
// expect(wrapper.find('div.display-username').text()).toEqual('@username')
|
||||
// })
|
||||
// })
|
||||
})
|
||||
|
||||
describe('send createContributionLink with error', () => {
|
||||
beforeEach(() => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
|
||||
wrapper = Wrapper()
|
||||
wrapper.vm.onSubmit()
|
||||
it('toasts a succes message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('https://localhost/redeem/CL-1a2345678')
|
||||
})
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('contributionLink.noStartDate')
|
||||
describe('send createContributionLink with error', () => {
|
||||
beforeEach(async () => {
|
||||
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
|
||||
await wrapper
|
||||
.findAllComponents({ name: 'BFormDatepicker' })
|
||||
.at(0)
|
||||
.vm.$emit('input', '2022-6-18')
|
||||
await wrapper
|
||||
.findAllComponents({ name: 'BFormDatepicker' })
|
||||
.at(1)
|
||||
.vm.$emit('input', '2022-7-18')
|
||||
await wrapper.find('input.test-name').setValue('test name')
|
||||
await wrapper.find('textarea.test-memo').setValue('test memo')
|
||||
await wrapper.find('input.test-amount').setValue('100')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
it('toasts an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('OUCH!')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div class="contribution-link-form">
|
||||
<div v-if="updateData" class="text-light bg-info p-3">
|
||||
{{ updateData }}
|
||||
</div>
|
||||
<b-form class="m-5" @submit.prevent="onSubmit" ref="contributionLinkForm">
|
||||
<!-- Date -->
|
||||
<b-row>
|
||||
@ -68,34 +65,32 @@
|
||||
class="test-amount"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
<b-collapse id="collapse-2">
|
||||
<b-jumbotron>
|
||||
<b-row class="mb-4">
|
||||
<b-col>
|
||||
<!-- Cycle -->
|
||||
<label for="cycle">{{ $t('contributionLink.cycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.cycle"
|
||||
:options="cycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<!-- maxPerCycle -->
|
||||
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.maxPerCycle"
|
||||
:options="maxPerCycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="mb-4">
|
||||
<b-col>
|
||||
<!-- Cycle -->
|
||||
<label for="cycle">{{ $t('contributionLink.cycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.cycle"
|
||||
:options="cycle"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<!-- maxPerCycle -->
|
||||
<label for="maxPerCycle">{{ $t('contributionLink.maxPerCycle') }}</label>
|
||||
<b-form-select
|
||||
v-model="form.maxPerCycle"
|
||||
:options="maxPerCycle"
|
||||
:disabled="disabled"
|
||||
class="mb-3"
|
||||
size="lg"
|
||||
></b-form-select>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<!-- Max amount -->
|
||||
<!-- Max amount -->
|
||||
<!--
|
||||
<b-form-group :label="$t('contributionLink.maximumAmount')">
|
||||
<b-form-input
|
||||
v-model="form.maxAmountPerMonth"
|
||||
@ -105,8 +100,7 @@
|
||||
placeholder="0"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
</b-jumbotron>
|
||||
</b-collapse>
|
||||
-->
|
||||
<div class="mt-6">
|
||||
<b-button type="submit" variant="primary">{{ $t('contributionLink.create') }}</b-button>
|
||||
<b-button type="reset" variant="danger" @click.prevent="onReset">
|
||||
@ -143,18 +137,18 @@ export default {
|
||||
min: new Date(),
|
||||
cycle: [
|
||||
{ value: 'ONCE', text: this.$t('contributionLink.options.cycle.once') },
|
||||
{ value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
|
||||
{ value: 'daily', text: this.$t('contributionLink.options.cycle.daily') },
|
||||
{ value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
|
||||
{ value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
|
||||
{ value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
|
||||
// { value: 'hourly', text: this.$t('contributionLink.options.cycle.hourly') },
|
||||
{ value: 'DAILY', text: this.$t('contributionLink.options.cycle.daily') },
|
||||
// { value: 'weekly', text: this.$t('contributionLink.options.cycle.weekly') },
|
||||
// { value: 'monthly', text: this.$t('contributionLink.options.cycle.monthly') },
|
||||
// { value: 'yearly', text: this.$t('contributionLink.options.cycle.yearly') },
|
||||
],
|
||||
maxPerCycle: [
|
||||
{ value: '1', text: '1 x' },
|
||||
{ value: '2', text: '2 x' },
|
||||
{ value: '3', text: '3 x' },
|
||||
{ value: '4', text: '4 x' },
|
||||
{ value: '5', text: '5 x' },
|
||||
// { value: '2', text: '2 x' },
|
||||
// { value: '3', text: '3 x' },
|
||||
// { value: '4', text: '4 x' },
|
||||
// { value: '5', text: '5 x' },
|
||||
],
|
||||
}
|
||||
},
|
||||
@ -163,7 +157,6 @@ export default {
|
||||
if (this.form.validFrom === null)
|
||||
return this.toastError(this.$t('contributionLink.noStartDate'))
|
||||
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
||||
// alert(JSON.stringify(this.form))
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -182,6 +175,8 @@ export default {
|
||||
this.link = result.data.createContributionLink.link
|
||||
this.toastSuccess(this.link)
|
||||
this.onReset()
|
||||
this.$root.$emit('bv::toggle::collapse', 'newContribution')
|
||||
this.$emit('get-contribution-links')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
@ -194,12 +189,8 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
updateData() {
|
||||
return this.contributionLinkData
|
||||
},
|
||||
disabled() {
|
||||
if (this.form.cycle === 'ONCE') return true
|
||||
return false
|
||||
return true
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@ -9,6 +9,7 @@ const mockAPIcall = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
mutate: mockAPIcall,
|
||||
},
|
||||
@ -95,7 +96,7 @@ describe('ContributionLinkList', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
|
||||
expect(toastSuccessSpy).toBeCalledWith('contributionLink.deleted')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="contribution-link-list">
|
||||
<b-table striped hover :items="items" :fields="fields">
|
||||
<template #cell(delete)>
|
||||
<template #cell(delete)="data">
|
||||
<b-button
|
||||
variant="danger"
|
||||
size="md"
|
||||
class="mr-2 test-delete-link"
|
||||
@click="deleteContributionLink"
|
||||
@click="deleteContributionLink(data.item.id, data.item.name)"
|
||||
>
|
||||
<b-icon icon="trash" variant="light"></b-icon>
|
||||
</b-button>
|
||||
@ -34,7 +34,7 @@
|
||||
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
||||
</template>
|
||||
<b-card-text>
|
||||
{{ modalData }}
|
||||
{{ modalData.memo ? modalData.memo : '' }}
|
||||
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
||||
</b-card-text>
|
||||
<template #footer>
|
||||
@ -64,34 +64,56 @@ export default {
|
||||
'amount',
|
||||
{ key: 'cycle', label: this.$t('contributionLink.cycle') },
|
||||
{ key: 'maxPerCycle', label: this.$t('contributionLink.maxPerCycle') },
|
||||
{ key: 'validFrom', label: this.$t('contributionLink.validFrom') },
|
||||
{ key: 'validTo', label: this.$t('contributionLink.validTo') },
|
||||
{
|
||||
key: 'validFrom',
|
||||
label: this.$t('contributionLink.validFrom'),
|
||||
formatter: (value, key, item) => {
|
||||
if (value) {
|
||||
return this.$d(new Date(value))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'validTo',
|
||||
label: this.$t('contributionLink.validTo'),
|
||||
formatter: (value, key, item) => {
|
||||
if (value) {
|
||||
return this.$d(new Date(value))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
'delete',
|
||||
'edit',
|
||||
'show',
|
||||
],
|
||||
modalData: null,
|
||||
modalDataLink: null,
|
||||
modalData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteContributionLink() {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteContributionLink,
|
||||
variables: {
|
||||
id: this.id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess('TODO: request message deleted ')
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
})
|
||||
deleteContributionLink(id, name) {
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(this.$t('contributionLink.deleteNow', { name: name }))
|
||||
.then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteContributionLink,
|
||||
variables: {
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess(this.$t('contributionLink.deleted'))
|
||||
this.$emit('get-contribution-links')
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
})
|
||||
})
|
||||
},
|
||||
editContributionLink(row) {
|
||||
this.$emit('editContributionLinkData', row)
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
v-model="form.text"
|
||||
:placeholder="$t('contributionLink.memo')"
|
||||
rows="3"
|
||||
max-rows="6"
|
||||
></b-form-textarea>
|
||||
<b-row class="mt-4 mb-6">
|
||||
<b-col>
|
||||
|
||||
38
admin/src/components/ContributionMessages/LinkifyMessage.vue
Normal file
38
admin/src/components/ContributionMessages/LinkifyMessage.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<span v-for="({ type, text }, index) in linkifiedMessage" :key="index">
|
||||
<b-link v-if="type === 'link'" :to="text">{{ text }}</b-link>
|
||||
<span v-else>{{ text }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const LINK_REGEX_PATTERN =
|
||||
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*))/i
|
||||
|
||||
export default {
|
||||
name: 'LinkifyMessage',
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
linkifiedMessage() {
|
||||
const linkified = []
|
||||
let string = this.message
|
||||
let match
|
||||
while ((match = string.match(LINK_REGEX_PATTERN))) {
|
||||
if (match.index > 0)
|
||||
linkified.push({ type: 'text', text: string.substring(0, match.index) })
|
||||
linkified.push({ type: 'link', text: match[0] })
|
||||
string = string.substring(match.index + match[0].length)
|
||||
}
|
||||
if (string.length > 0) linkified.push({ type: 'text', text: string })
|
||||
return linkified
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -9,50 +9,184 @@ describe('ContributionMessagesListItem', () => {
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
$store: {
|
||||
state: {
|
||||
moderator: {
|
||||
id: 107,
|
||||
},
|
||||
}
|
||||
|
||||
describe('if message author has moderator role', () => {
|
||||
const propsData = {
|
||||
contributionId: 42,
|
||||
state: 'PENDING',
|
||||
message: {
|
||||
id: 111,
|
||||
message: 'Lorem ipsum?',
|
||||
createdAt: '2022-08-29T12:23:27.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
userId: 107,
|
||||
isModerator: true,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
contributionId: 42,
|
||||
state: 'PENDING0',
|
||||
message: {
|
||||
id: 111,
|
||||
message: 'asd asda sda sda',
|
||||
createdAt: '2022-08-29T12:23:27.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
userId: 107,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
const ModeratorItemWrapper = () => {
|
||||
return mount(ContributionMessagesListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(ContributionMessagesListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
describe('mount', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = ModeratorItemWrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .text-right.is-moderator', () => {
|
||||
expect(wrapper.find('div.text-right.is-moderator').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the complete user name', () => {
|
||||
expect(wrapper.find('div.text-right.is-moderator > span:nth-child(2)').text()).toBe(
|
||||
'Peter Lustig',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the message creation date', () => {
|
||||
expect(wrapper.find('div.text-right.is-moderator > span:nth-child(3)').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',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the message', () => {
|
||||
expect(wrapper.find('div.text-right.is-moderator > div:nth-child(5)').text()).toBe(
|
||||
'Lorem ipsum?',
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
describe('if message author does not have moderator role', () => {
|
||||
const propsData = {
|
||||
contributionId: 42,
|
||||
state: 'PENDING',
|
||||
message: {
|
||||
id: 113,
|
||||
message: 'Asda sdad ad asdasd, das Ass das Das. ',
|
||||
createdAt: '2022-08-29T12:25:34.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Bibi',
|
||||
userLastName: 'Bloxberg',
|
||||
userId: 108,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
|
||||
const ItemWrapper = () => {
|
||||
return mount(ContributionMessagesListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeAll(() => {
|
||||
wrapper = ItemWrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .text-left.is-not-moderator', () => {
|
||||
expect(wrapper.find('div.text-left.is-not-moderator').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',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the message creation date', () => {
|
||||
expect(wrapper.find('div.is-not-moderator.text-left > span:nth-child(3)').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(
|
||||
'Asda sdad ad asdasd, das Ass das Das.',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('links in contribtion message', () => {
|
||||
const propsData = {
|
||||
message: {
|
||||
id: 111,
|
||||
message: 'Lorem ipsum?',
|
||||
createdAt: '2022-08-29T12:23:27.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
userId: 107,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
|
||||
const ModeratorItemWrapper = () => {
|
||||
return mount(ContributionMessagesListItem, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
let messageField
|
||||
|
||||
describe('message of only one link', () => {
|
||||
beforeEach(() => {
|
||||
propsData.message.message = 'https://gradido.net/de/'
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
|
||||
})
|
||||
|
||||
it('contains the link as text', () => {
|
||||
expect(messageField.text()).toBe('https://gradido.net/de/')
|
||||
})
|
||||
|
||||
it('contains a link to the given address', () => {
|
||||
expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/')
|
||||
})
|
||||
})
|
||||
|
||||
it('has a DIV .contribution-messages-list-item', () => {
|
||||
expect(wrapper.find('div.contribution-messages-list-item').exists()).toBe(true)
|
||||
})
|
||||
describe('message with text and two links', () => {
|
||||
beforeEach(() => {
|
||||
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)')
|
||||
})
|
||||
|
||||
it('props.message.default', () => {
|
||||
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
|
||||
it('contains the whole text', () => {
|
||||
expect(messageField.text())
|
||||
.toBe(`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`)
|
||||
})
|
||||
|
||||
it('contains the two links', () => {
|
||||
expect(messageField.findAll('a').at(0).attributes('href')).toBe('https://gradido.net/de/')
|
||||
expect(messageField.findAll('a').at(1).attributes('href')).toBe(
|
||||
'https://github.com/gradido/gradido',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,27 +1,49 @@
|
||||
<template>
|
||||
<div class="contribution-messages-list-item">
|
||||
<is-moderator v-if="message.isModerator" :message="message"></is-moderator>
|
||||
<is-not-moderator v-else :message="message"></is-not-moderator>
|
||||
<div v-if="message.isModerator" class="text-right is-moderator">
|
||||
<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>
|
||||
<linkify-message :message="message.message"></linkify-message>
|
||||
</div>
|
||||
<div v-else class="text-left is-not-moderator">
|
||||
<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>
|
||||
<linkify-message :message="message.message"></linkify-message>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import IsModerator from '@/components/ContributionMessages/slots/IsModerator.vue'
|
||||
import IsNotModerator from '@/components/ContributionMessages/slots/IsNotModerator.vue'
|
||||
import LinkifyMessage from '@/components/ContributionMessages/LinkifyMessage.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionMessagesListItem',
|
||||
components: {
|
||||
IsModerator,
|
||||
IsNotModerator,
|
||||
LinkifyMessage,
|
||||
},
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
</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); */
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import IsModerator from './IsModerator.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('IsModerator', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
message: {
|
||||
id: 111,
|
||||
message: 'asd asda sda sda',
|
||||
createdAt: '2022-08-29T12:23:27.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Peter',
|
||||
userLastName: 'Lustig',
|
||||
userId: 107,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(IsModerator, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .slot-is-moderator', () => {
|
||||
expect(wrapper.find('div.slot-is-moderator').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('props.message.default', () => {
|
||||
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<div class="slot-is-moderator">
|
||||
<div class="text-right">
|
||||
<b-avatar square :text="initialLetters" 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>
|
||||
<div class="mt-2">{{ message.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
initialLetters() {
|
||||
return `${this.message.userFirstName[0]} ${this.message.userLastName[0]}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.slot-is-moderator {
|
||||
clear: both;
|
||||
float: right;
|
||||
width: 75%;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -1,49 +0,0 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import IsNotModerator from './IsNotModerator.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('IsNotModerator', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$d: jest.fn((d) => d),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
message: {
|
||||
id: 113,
|
||||
message: 'asda sdad ad asdasd ',
|
||||
createdAt: '2022-08-29T12:25:34.000Z',
|
||||
updatedAt: null,
|
||||
type: 'DIALOG',
|
||||
userFirstName: 'Bibi',
|
||||
userLastName: 'Bloxberg',
|
||||
userId: 108,
|
||||
__typename: 'ContributionMessage',
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(IsNotModerator, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .slot-is-not-moderator', () => {
|
||||
expect(wrapper.find('div.slot-is-not-moderator').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('props.message.default', () => {
|
||||
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div class="slot-is-not-moderator">
|
||||
<div>
|
||||
<b-avatar :text="initialLetters" 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>
|
||||
<div class="mt-2">{{ message.message }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
message: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
initialLetters() {
|
||||
return `${this.message.userFirstName[0]} ${this.message.userLastName[0]}`
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.slot-is-not-moderator {
|
||||
clear: both;
|
||||
width: 75%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@ -6,30 +6,29 @@ const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
creationTransactionList: [
|
||||
{
|
||||
id: 1,
|
||||
amount: 100,
|
||||
balanceDate: 0,
|
||||
creationDate: new Date(),
|
||||
memo: 'Testing',
|
||||
linkedUser: {
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
creationTransactionList: {
|
||||
contributionCount: 2,
|
||||
contributionList: [
|
||||
{
|
||||
id: 1,
|
||||
amount: 5.8,
|
||||
createdAt: '2022-09-21T11:09:51.000Z',
|
||||
confirmedAt: null,
|
||||
contributionDate: '2022-08-01T00:00:00.000Z',
|
||||
memo: 'für deine Hilfe, Fräulein Rottenmeier',
|
||||
state: 'PENDING',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
amount: 200,
|
||||
balanceDate: 0,
|
||||
creationDate: new Date(),
|
||||
memo: 'Testing 2',
|
||||
linkedUser: {
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
{
|
||||
id: 2,
|
||||
amount: '47',
|
||||
createdAt: '2022-09-21T11:09:28.000Z',
|
||||
confirmedAt: '2022-09-21T11:09:28.000Z',
|
||||
contributionDate: '2022-08-01T00:00:00.000Z',
|
||||
memo: 'für deine Hilfe, Frau Holle',
|
||||
state: 'CONFIRMED',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -43,7 +42,7 @@ const mocks = {
|
||||
|
||||
const propsData = {
|
||||
userId: 1,
|
||||
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
||||
fields: ['createdAt', 'contributionDate', 'confirmedAt', 'amount', 'memo'],
|
||||
}
|
||||
|
||||
describe('CreationTransactionList', () => {
|
||||
@ -63,7 +62,7 @@ describe('CreationTransactionList', () => {
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
pageSize: 10,
|
||||
order: 'DESC',
|
||||
userId: 1,
|
||||
},
|
||||
|
||||
@ -1,7 +1,44 @@
|
||||
<template>
|
||||
<div class="component-creation-transaction-list">
|
||||
<div class="h3">{{ $t('transactionlist.title') }}</div>
|
||||
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||
<b-table striped hover :fields="fields" :items="items">
|
||||
<template #cell(contributionDate)="data">
|
||||
<div class="font-weight-bold">
|
||||
{{ $d(new Date(data.item.contributionDate), 'month') }}
|
||||
</div>
|
||||
<div>{{ $d(new Date(data.item.contributionDate)) }}</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<div>
|
||||
<b-pagination
|
||||
pills
|
||||
size="lg"
|
||||
v-model="currentPage"
|
||||
:per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
<b-button v-b-toggle.collapse-1 variant="light" size="sm">{{ $t('help.help') }}</b-button>
|
||||
<b-collapse id="collapse-1" class="mt-2">
|
||||
<div>
|
||||
{{ $t('transactionlist.submitted') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.submitted') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('transactionlist.period') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.periods') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('transactionlist.confirmed') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.confirmed') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('transactionlist.state') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.state') }}
|
||||
</div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -13,14 +50,37 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
perPage: 10,
|
||||
fields: [
|
||||
{
|
||||
key: 'creationDate',
|
||||
label: this.$t('transactionlist.date'),
|
||||
key: 'createdAt',
|
||||
label: this.$t('transactionlist.submitted'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'contributionDate',
|
||||
label: this.$t('transactionlist.period'),
|
||||
},
|
||||
{
|
||||
key: 'confirmedAt',
|
||||
label: this.$t('transactionlist.confirmed'),
|
||||
formatter: (value, key, item) => {
|
||||
if (value) {
|
||||
return this.$d(new Date(value))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: this.$t('transactionlist.state'),
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: this.$t('transactionlist.amount'),
|
||||
@ -28,23 +88,8 @@ export default {
|
||||
return `${value} GDD`
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'linkedUser',
|
||||
label: this.$t('transactionlist.community'),
|
||||
formatter: (value, key, item) => {
|
||||
return `${value.firstName} ${value.lastName}`
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: this.$t('transactionlist.memo') },
|
||||
{
|
||||
key: 'balanceDate',
|
||||
label: this.$t('transactionlist.balanceDate'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -53,14 +98,15 @@ export default {
|
||||
.query({
|
||||
query: creationTransactionList,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.perPage,
|
||||
order: 'DESC',
|
||||
userId: parseInt(this.userId),
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.items = result.data.creationTransactionList
|
||||
this.rows = result.data.creationTransactionList.contributionCount
|
||||
this.items = result.data.creationTransactionList.contributionList
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
@ -70,5 +116,10 @@ export default {
|
||||
created() {
|
||||
this.getTransactions()
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.getTransactions()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
value-field="item"
|
||||
text-field="name"
|
||||
name="month-selection"
|
||||
:disabled="true"
|
||||
></b-form-radio-group>
|
||||
</b-row>
|
||||
<div class="m-4">
|
||||
|
||||
@ -3,11 +3,15 @@ import NavBar from './NavBar.vue'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloMutateMock = jest.fn()
|
||||
const storeDispatchMock = jest.fn()
|
||||
const routerPushMock = jest.fn()
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$apollo: {
|
||||
mutate: apolloMutateMock,
|
||||
},
|
||||
$store: {
|
||||
state: {
|
||||
openCreations: 1,
|
||||
@ -69,5 +73,9 @@ describe('NavBar', () => {
|
||||
it('dispatches logout to store', () => {
|
||||
expect(storeDispatchMock).toBeCalledWith('logout')
|
||||
})
|
||||
|
||||
it('has called logout mutation', () => {
|
||||
expect(apolloMutateMock).toBeCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -28,14 +28,18 @@
|
||||
</template>
|
||||
<script>
|
||||
import CONFIG from '../config'
|
||||
import { logout } from '../graphql/logout'
|
||||
|
||||
export default {
|
||||
name: 'navbar',
|
||||
methods: {
|
||||
logout() {
|
||||
async logout() {
|
||||
window.location.assign(CONFIG.WALLET_URL)
|
||||
// window.location = CONFIG.WALLET_URL
|
||||
this.$store.dispatch('logout')
|
||||
await this.$apollo.mutate({
|
||||
mutation: logout,
|
||||
})
|
||||
},
|
||||
wallet() {
|
||||
window.location = CONFIG.WALLET_AUTH_URL.replace('{token}', this.$store.state.token)
|
||||
|
||||
@ -8,14 +8,15 @@ export const creationTransactionList = gql`
|
||||
order: $order
|
||||
userId: $userId
|
||||
) {
|
||||
id
|
||||
amount
|
||||
balanceDate
|
||||
creationDate
|
||||
memo
|
||||
linkedUser {
|
||||
firstName
|
||||
lastName
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
createdAt
|
||||
confirmedAt
|
||||
contributionDate
|
||||
memo
|
||||
state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
admin/src/graphql/logout.js
Normal file
7
admin/src/graphql/logout.js
Normal file
@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const logout = gql`
|
||||
mutation {
|
||||
logout
|
||||
}
|
||||
`
|
||||
@ -1,18 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const showContributionLink = gql`
|
||||
query ($id: Int!) {
|
||||
showContributionLink {
|
||||
id
|
||||
validFrom
|
||||
validTo
|
||||
name
|
||||
memo
|
||||
amount
|
||||
cycle
|
||||
maxPerCycle
|
||||
maxAmountPerMonth
|
||||
code
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -7,8 +7,8 @@
|
||||
"contributionLinks": "Beitragslinks",
|
||||
"create": "Anlegen",
|
||||
"cycle": "Zyklus",
|
||||
"deleteNow": "Automatische Creations wirklich löschen?",
|
||||
"maximumAmount": "maximaler Betrag",
|
||||
"deleted": "Automatische Schöpfung gelöscht!",
|
||||
"deleteNow": "Automatische Creations '{name}' wirklich löschen?",
|
||||
"maxPerCycle": "Wiederholungen",
|
||||
"memo": "Nachricht",
|
||||
"name": "Name",
|
||||
@ -20,11 +20,7 @@
|
||||
"options": {
|
||||
"cycle": {
|
||||
"daily": "täglich",
|
||||
"hourly": "stündlich",
|
||||
"monthly": "monatlich",
|
||||
"once": "einmalig",
|
||||
"weekly": "wöchentlich",
|
||||
"yearly": "jährlich"
|
||||
"once": "einmalig"
|
||||
}
|
||||
},
|
||||
"validFrom": "Startdatum",
|
||||
@ -74,10 +70,20 @@
|
||||
"submit": "Senden"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"help": {
|
||||
"help": "Hilfe",
|
||||
"transactionlist": {
|
||||
"confirmed": "Wann wurde es von einem Moderator / Admin bestätigt.",
|
||||
"periods": "Für welchen Zeitraum wurde vom Mitglied eingereicht.",
|
||||
"state": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
|
||||
"submitted": "Wann wurde es vom Mitglied eingereicht"
|
||||
}
|
||||
},
|
||||
"hide_details": "Details verbergen",
|
||||
"lastname": "Nachname",
|
||||
"math": {
|
||||
"colon": ":",
|
||||
"equals": "=",
|
||||
"exclaim": "!",
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
@ -133,10 +139,11 @@
|
||||
},
|
||||
"transactionlist": {
|
||||
"amount": "Betrag",
|
||||
"balanceDate": "Schöpfungsdatum",
|
||||
"community": "Gemeinschaft",
|
||||
"date": "Datum",
|
||||
"confirmed": "Bestätigt",
|
||||
"memo": "Nachricht",
|
||||
"period": "Zeitraum",
|
||||
"state": "Status",
|
||||
"submitted": "Eingereicht",
|
||||
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
||||
},
|
||||
"undelete_user": "Nutzer wiederherstellen",
|
||||
|
||||
@ -7,8 +7,8 @@
|
||||
"contributionLinks": "Contribution Links",
|
||||
"create": "Create",
|
||||
"cycle": "Cycle",
|
||||
"deleteNow": "Do you really delete automatic creations?",
|
||||
"maximumAmount": "Maximum amount",
|
||||
"deleted": "Automatic creation deleted!",
|
||||
"deleteNow": "Do you really delete automatic creations '{name}'?",
|
||||
"maxPerCycle": "Repetition",
|
||||
"memo": "Memo",
|
||||
"name": "Name",
|
||||
@ -20,11 +20,7 @@
|
||||
"options": {
|
||||
"cycle": {
|
||||
"daily": "daily",
|
||||
"hourly": "hourly",
|
||||
"monthly": "monthly",
|
||||
"once": "once",
|
||||
"weekly": "weekly",
|
||||
"yearly": "yearly"
|
||||
"once": "once"
|
||||
}
|
||||
},
|
||||
"validFrom": "Start-date",
|
||||
@ -74,10 +70,20 @@
|
||||
"submit": "Send"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"help": {
|
||||
"help": "Help",
|
||||
"transactionlist": {
|
||||
"confirmed": "When was it confirmed by a moderator / admin.",
|
||||
"periods": "For what period was it submitted by the member.",
|
||||
"state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = denied, CONFIRMED = confirmed]",
|
||||
"submitted": "When was it submitted by the member"
|
||||
}
|
||||
},
|
||||
"hide_details": "Hide details",
|
||||
"lastname": "Lastname",
|
||||
"math": {
|
||||
"colon": ":",
|
||||
"equals": "=",
|
||||
"exclaim": "!",
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
@ -133,10 +139,11 @@
|
||||
},
|
||||
"transactionlist": {
|
||||
"amount": "Amount",
|
||||
"balanceDate": "Creation date",
|
||||
"community": "Community",
|
||||
"date": "Date",
|
||||
"confirmed": "Confirmed",
|
||||
"memo": "Message",
|
||||
"period": "Period",
|
||||
"state": "State",
|
||||
"submitted": "Submitted",
|
||||
"title": "All creation-transactions for the user"
|
||||
},
|
||||
"undelete_user": "Undelete User",
|
||||
|
||||
@ -78,6 +78,7 @@ const storeCommitMock = jest.fn()
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$n: jest.fn((n) => n),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
|
||||
@ -28,7 +28,11 @@
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<contribution-link :items="items" :count="count" />
|
||||
<contribution-link
|
||||
:items="items"
|
||||
:count="count"
|
||||
@get-contribution-links="getContributionLinks"
|
||||
/>
|
||||
<community-statistic class="mt-5" v-model="statistics" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -10,6 +10,7 @@ const authLink = new ApolloLink((operation, forward) => {
|
||||
operation.setContext({
|
||||
headers: {
|
||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||
clientRequestTime: new Date().toString(),
|
||||
},
|
||||
})
|
||||
return forward(operation).map((response) => {
|
||||
|
||||
@ -94,6 +94,7 @@ describe('apolloProvider', () => {
|
||||
expect(setContextMock).toBeCalledWith({
|
||||
headers: {
|
||||
Authorization: 'Bearer some-token',
|
||||
clientRequestTime: expect.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -109,6 +110,7 @@ describe('apolloProvider', () => {
|
||||
expect(setContextMock).toBeCalledWith({
|
||||
headers: {
|
||||
Authorization: '',
|
||||
clientRequestTime: expect.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v9.2022-07-07
|
||||
CONFIG_VERSION=v10.2022-09-20
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
@ -37,6 +37,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_TEST_MODUS=false
|
||||
EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
|
||||
@ -36,6 +36,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=$EMAIL
|
||||
EMAIL_TEST_MODUS=$EMAIL_TEST_MODUS
|
||||
EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER
|
||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||
EMAIL_SENDER=$EMAIL_SENDER
|
||||
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
||||
|
||||
@ -5,6 +5,7 @@ module.exports = {
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||
setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'],
|
||||
modulePathIgnorePatterns: ['<rootDir>/build/'],
|
||||
moduleNameMapper: {
|
||||
'@/(.*)': '<rootDir>/src/$1',
|
||||
|
||||
@ -5,41 +5,66 @@
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/backend/access.log",
|
||||
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_"
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"apollo":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/backend/apollo.log",
|
||||
"pattern": "%d{ISO8601} %p %c %m",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_"
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"backend":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/backend/backend.log",
|
||||
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_"
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"klicktipp":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/backend/klicktipp.log",
|
||||
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_"
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"errorFile":
|
||||
{
|
||||
"type": "dateFile",
|
||||
"filename": "../logs/backend/errors.log",
|
||||
"pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m",
|
||||
"pattern": "yyyy-MM-dd",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
},
|
||||
"keepFileExt" : true,
|
||||
"fileNameSep" : "_"
|
||||
"fileNameSep" : "_",
|
||||
"numBackups" : 30
|
||||
},
|
||||
"errors":
|
||||
{
|
||||
@ -52,7 +77,7 @@
|
||||
"type": "stdout",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c %X{user} %f:%l %m"
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
}
|
||||
},
|
||||
"apolloOut":
|
||||
@ -60,7 +85,7 @@
|
||||
"type": "stdout",
|
||||
"layout":
|
||||
{
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c %m"
|
||||
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.12.1",
|
||||
"version": "1.13.1",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0048-add_is_moderator_to_contribution_messages',
|
||||
DB_VERSION: '0051-add_delete_by_to_contributions',
|
||||
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: 'v9.2022-07-07',
|
||||
EXPECTED: 'v10.2022-09-20',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -67,6 +67,8 @@ const loginServer = {
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || false,
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||
|
||||
@ -11,47 +11,67 @@ export class EventBasicUserId extends EventBasic {
|
||||
}
|
||||
|
||||
export class EventBasicTx extends EventBasicUserId {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
transactionId: number
|
||||
amount: decimal
|
||||
}
|
||||
|
||||
export class EventBasicTxX extends EventBasicTx {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
}
|
||||
|
||||
export class EventBasicCt extends EventBasicUserId {
|
||||
contributionId: number
|
||||
amount: decimal
|
||||
}
|
||||
|
||||
export class EventBasicCtX extends EventBasicCt {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
}
|
||||
|
||||
export class EventBasicRedeem extends EventBasicUserId {
|
||||
transactionId?: number
|
||||
contributionId?: number
|
||||
}
|
||||
|
||||
export class EventBasicCtMsg extends EventBasicCt {
|
||||
messageId: number
|
||||
}
|
||||
|
||||
export class EventVisitGradido extends EventBasic {}
|
||||
export class EventRegister extends EventBasicUserId {}
|
||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||
export class EventVerifyRedeem extends EventBasicRedeem {}
|
||||
export class EventInactiveAccount extends EventBasicUserId {}
|
||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
|
||||
export class EventSendForgotPasswordEmail extends EventBasicUserId {}
|
||||
export class EventSendTransactionSendEmail extends EventBasicTxX {}
|
||||
export class EventSendTransactionReceiveEmail extends EventBasicTxX {}
|
||||
export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {}
|
||||
export class EventSendAddedContributionEmail extends EventBasicCt {}
|
||||
export class EventSendContributionConfirmEmail extends EventBasicCt {}
|
||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||
export class EventLogin extends EventBasicUserId {}
|
||||
export class EventLogout extends EventBasicUserId {}
|
||||
export class EventRedeemLogin extends EventBasicRedeem {}
|
||||
export class EventActivateAccount extends EventBasicUserId {}
|
||||
export class EventPasswordChange extends EventBasicUserId {}
|
||||
export class EventTransactionSend extends EventBasicTx {}
|
||||
export class EventTransactionSendRedeem extends EventBasicTx {}
|
||||
export class EventTransactionRepeateRedeem extends EventBasicTx {}
|
||||
export class EventTransactionCreation extends EventBasicUserId {
|
||||
transactionId: number
|
||||
amount: decimal
|
||||
}
|
||||
export class EventTransactionReceive extends EventBasicTx {}
|
||||
export class EventTransactionReceiveRedeem extends EventBasicTx {}
|
||||
export class EventTransactionSend extends EventBasicTxX {}
|
||||
export class EventTransactionSendRedeem extends EventBasicTxX {}
|
||||
export class EventTransactionRepeateRedeem extends EventBasicTxX {}
|
||||
export class EventTransactionCreation extends EventBasicTx {}
|
||||
export class EventTransactionReceive extends EventBasicTxX {}
|
||||
export class EventTransactionReceiveRedeem extends EventBasicTxX {}
|
||||
export class EventContributionCreate extends EventBasicCt {}
|
||||
export class EventContributionConfirm extends EventBasicCt {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
}
|
||||
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
|
||||
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
|
||||
export class EventContributionDelete extends EventBasicCt {}
|
||||
export class EventContributionUpdate extends EventBasicCt {}
|
||||
export class EventContributionConfirm extends EventBasicCtX {}
|
||||
export class EventContributionDeny extends EventBasicCtX {}
|
||||
export class EventContributionLinkDefine extends EventBasicCt {}
|
||||
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
|
||||
|
||||
@ -99,6 +119,13 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventVerifyRedeem(ev: EventVerifyRedeem): Event {
|
||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||
this.type = EventProtocolType.VERIFY_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.INACTIVE_ACCOUNT
|
||||
@ -113,6 +140,57 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendAccountMultiRegistrationEmail(
|
||||
ev: EventSendAccountMultiRegistrationEmail,
|
||||
): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event {
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event {
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event {
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.CONFIRM_EMAIL
|
||||
@ -134,6 +212,13 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventLogout(ev: EventLogout): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.LOGOUT
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
|
||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||
this.type = EventProtocolType.REDEEM_LOGIN
|
||||
@ -156,44 +241,42 @@ export class Event {
|
||||
}
|
||||
|
||||
public setEventTransactionSend(ev: EventTransactionSend): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.TRANSACTION_SEND
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
if (ev.transactionId) this.transactionId = ev.transactionId
|
||||
if (ev.amount) this.amount = ev.amount
|
||||
this.setByBasicTx(ev.userId, ev.transactionId, ev.amount)
|
||||
this.type = EventProtocolType.TRANSACTION_CREATION
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.TRANSACTION_RECEIVE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
|
||||
this.setByBasicTx(ev.userId, ev.xUserId, ev.xCommunityId, ev.transactionId, ev.amount)
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
|
||||
|
||||
return this
|
||||
@ -206,15 +289,48 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
|
||||
public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event {
|
||||
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
|
||||
this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventAdminCreateContributionMessage(ev: EventAdminCreateContributionMessage): Event {
|
||||
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
|
||||
this.type = EventProtocolType.ADMIN_CREATE_CONTRIBUTION_MESSAGE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionDelete(ev: EventContributionDelete): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
if (ev.xUserId) this.xUserId = ev.xUserId
|
||||
if (ev.xCommunityId) this.xCommunityId = ev.xCommunityId
|
||||
this.type = EventProtocolType.CONTRIBUTION_DELETE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionUpdate(ev: EventContributionUpdate): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.CONTRIBUTION_UPDATE
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
|
||||
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionDeny(ev: EventContributionDeny): Event {
|
||||
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.CONTRIBUTION_DENY
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
|
||||
@ -236,26 +352,58 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicTx(
|
||||
userId: number,
|
||||
xUserId?: number,
|
||||
xCommunityId?: number,
|
||||
transactionId?: number,
|
||||
amount?: decimal,
|
||||
): Event {
|
||||
setByBasicTx(userId: number, transactionId: number, amount: decimal): Event {
|
||||
this.setByBasicUser(userId)
|
||||
if (xUserId) this.xUserId = xUserId
|
||||
if (xCommunityId) this.xCommunityId = xCommunityId
|
||||
if (transactionId) this.transactionId = transactionId
|
||||
if (amount) this.amount = amount
|
||||
this.transactionId = transactionId
|
||||
this.amount = amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicCt(userId: number, contributionId: number, amount?: decimal): Event {
|
||||
setByBasicTxX(
|
||||
userId: number,
|
||||
transactionId: number,
|
||||
amount: decimal,
|
||||
xUserId: number,
|
||||
xCommunityId: number,
|
||||
): Event {
|
||||
this.setByBasicTx(userId, transactionId, amount)
|
||||
this.xUserId = xUserId
|
||||
this.xCommunityId = xCommunityId
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicCt(userId: number, contributionId: number, amount: decimal): Event {
|
||||
this.setByBasicUser(userId)
|
||||
if (contributionId) this.contributionId = contributionId
|
||||
if (amount) this.amount = amount
|
||||
this.contributionId = contributionId
|
||||
this.amount = amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicCtMsg(
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: decimal,
|
||||
messageId: number,
|
||||
): Event {
|
||||
this.setByBasicCt(userId, contributionId, amount)
|
||||
this.messageId = messageId
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByBasicCtX(
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: decimal,
|
||||
xUserId: number,
|
||||
xCommunityId: number,
|
||||
): Event {
|
||||
this.setByBasicCt(userId, contributionId, amount)
|
||||
this.xUserId = xUserId
|
||||
this.xCommunityId = xCommunityId
|
||||
|
||||
return this
|
||||
}
|
||||
@ -268,27 +416,6 @@ export class Event {
|
||||
return this
|
||||
}
|
||||
|
||||
setByEventTransactionCreation(event: EventTransactionCreation): Event {
|
||||
this.type = event.type
|
||||
this.createdAt = event.createdAt
|
||||
this.userId = event.userId
|
||||
this.transactionId = event.transactionId
|
||||
this.amount = event.amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
setByEventContributionConfirm(event: EventContributionConfirm): Event {
|
||||
this.type = event.type
|
||||
this.createdAt = event.createdAt
|
||||
this.userId = event.userId
|
||||
this.xUserId = event.xUserId
|
||||
this.xCommunityId = event.xCommunityId
|
||||
this.amount = event.amount
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
id: number
|
||||
type: string
|
||||
createdAt: Date
|
||||
@ -298,4 +425,5 @@ export class Event {
|
||||
transactionId?: number
|
||||
contributionId?: number
|
||||
amount?: decimal
|
||||
messageId?: number
|
||||
}
|
||||
|
||||
@ -3,22 +3,36 @@ export enum EventProtocolType {
|
||||
VISIT_GRADIDO = 'VISIT_GRADIDO',
|
||||
REGISTER = 'REGISTER',
|
||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||
VERIFY_REDEEM = 'VERIFY_REDEEM',
|
||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
|
||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||
LOGIN = 'LOGIN',
|
||||
LOGOUT = 'LOGOUT',
|
||||
REDEEM_LOGIN = 'REDEEM_LOGIN',
|
||||
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
|
||||
SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
|
||||
PASSWORD_CHANGE = 'PASSWORD_CHANGE',
|
||||
SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
|
||||
SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
|
||||
TRANSACTION_SEND = 'TRANSACTION_SEND',
|
||||
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
|
||||
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
|
||||
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
|
||||
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
|
||||
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
|
||||
SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
|
||||
SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
|
||||
SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
|
||||
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
|
||||
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
|
||||
CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
|
||||
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
|
||||
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
|
||||
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
|
||||
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
|
||||
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
|
||||
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
||||
|
||||
// TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests
|
||||
// TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
const userRepository = await getCustomRepository(UserRepository)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
try {
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
context.user = user
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
// lowercase values are not implemented yet
|
||||
export enum ContributionCycleType {
|
||||
ONCE = 'once',
|
||||
ONCE = 'ONCE',
|
||||
HOUR = 'hour',
|
||||
TWO_HOURS = 'two_hours',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
EIGHT_HOURS = 'eight_hours',
|
||||
HALF_DAY = 'half_day',
|
||||
DAY = 'day',
|
||||
DAILY = 'DAILY',
|
||||
TWO_DAYS = 'two_days',
|
||||
THREE_DAYS = 'three_days',
|
||||
FOUR_DAYS = 'four_days',
|
||||
|
||||
11
backend/src/graphql/enum/UserContactType.ts
Normal file
11
backend/src/graphql/enum/UserContactType.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { registerEnumType } from 'type-graphql'
|
||||
|
||||
export enum UserContactType {
|
||||
USER_CONTACT_EMAIL = 'EMAIL',
|
||||
USER_CONTACT_PHONE = 'PHONE',
|
||||
}
|
||||
|
||||
registerEnumType(UserContactType, {
|
||||
name: 'UserContactType', // this one is mandatory
|
||||
description: 'Type of the user contact', // this one is optional
|
||||
})
|
||||
@ -5,7 +5,7 @@ import { User } from '@entity/User'
|
||||
|
||||
@ObjectType()
|
||||
export class Contribution {
|
||||
constructor(contribution: dbContribution, user: User) {
|
||||
constructor(contribution: dbContribution, user?: User | null) {
|
||||
this.id = contribution.id
|
||||
this.firstName = user ? user.firstName : null
|
||||
this.lastName = user ? user.lastName : null
|
||||
|
||||
@ -13,7 +13,7 @@ export class UnconfirmedContribution {
|
||||
this.date = contribution.contributionDate
|
||||
this.firstName = user ? user.firstName : ''
|
||||
this.lastName = user ? user.lastName : ''
|
||||
this.email = user ? user.email : ''
|
||||
this.email = user ? user.emailContact.email : ''
|
||||
this.moderator = contribution.moderatorId
|
||||
this.creation = creations
|
||||
this.state = contribution.contributionStatus
|
||||
|
||||
@ -3,6 +3,7 @@ import { KlickTipp } from './KlickTipp'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||
import { UserContact } from './UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
@ -10,12 +11,16 @@ export class User {
|
||||
this.id = user.id
|
||||
this.gradidoID = user.gradidoID
|
||||
this.alias = user.alias
|
||||
this.email = user.email
|
||||
this.emailId = user.emailId
|
||||
if (user.emailContact) {
|
||||
this.email = user.emailContact.email
|
||||
this.emailContact = new UserContact(user.emailContact)
|
||||
this.emailChecked = user.emailContact.emailChecked
|
||||
}
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.deletedAt = user.deletedAt
|
||||
this.createdAt = user.createdAt
|
||||
this.emailChecked = user.emailChecked
|
||||
this.language = user.language
|
||||
this.publisherId = user.publisherId
|
||||
this.isAdmin = user.isAdmin
|
||||
@ -34,12 +39,18 @@ export class User {
|
||||
gradidoID: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
alias: string
|
||||
alias?: string
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
emailId: number | null
|
||||
|
||||
// TODO privacy issue here
|
||||
@Field(() => String)
|
||||
@Field(() => String, { nullable: true })
|
||||
email: string
|
||||
|
||||
@Field(() => UserContact)
|
||||
emailContact: UserContact
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
|
||||
@ -6,11 +6,11 @@ import { User } from '@entity/User'
|
||||
export class UserAdmin {
|
||||
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||
this.userId = user.id
|
||||
this.email = user.email
|
||||
this.email = user.emailContact.email
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.creation = creation
|
||||
this.emailChecked = user.emailChecked
|
||||
this.emailChecked = user.emailContact.emailChecked
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
|
||||
56
backend/src/graphql/model/UserContact.ts
Normal file
56
backend/src/graphql/model/UserContact.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class UserContact {
|
||||
constructor(userContact: dbUserContact) {
|
||||
this.id = userContact.id
|
||||
this.type = userContact.type
|
||||
this.userId = userContact.userId
|
||||
this.email = userContact.email
|
||||
// this.emailVerificationCode = userContact.emailVerificationCode
|
||||
this.emailOptInTypeId = userContact.emailOptInTypeId
|
||||
this.emailResendCount = userContact.emailResendCount
|
||||
this.emailChecked = userContact.emailChecked
|
||||
this.phone = userContact.phone
|
||||
this.createdAt = userContact.createdAt
|
||||
this.updatedAt = userContact.updatedAt
|
||||
this.deletedAt = userContact.deletedAt
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
type: string
|
||||
|
||||
@Field(() => Number)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
// @Field(() => BigInt, { nullable: true })
|
||||
// emailVerificationCode: BigInt | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
emailOptInTypeId: number | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
emailResendCount: number | null
|
||||
|
||||
@Field(() => Boolean)
|
||||
emailChecked: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
phone: string | null
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
updatedAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
}
|
||||
@ -13,9 +13,11 @@ import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import {
|
||||
login,
|
||||
setUserRole,
|
||||
deleteUser,
|
||||
unDeleteUser,
|
||||
createContribution,
|
||||
adminCreateContribution,
|
||||
adminCreateContributions,
|
||||
adminUpdateContribution,
|
||||
@ -27,7 +29,6 @@ import {
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import {
|
||||
listUnconfirmedContributions,
|
||||
login,
|
||||
searchUsers,
|
||||
listTransactionLinksAdmin,
|
||||
listContributionLinks,
|
||||
@ -77,6 +78,7 @@ afterAll(async () => {
|
||||
let admin: User
|
||||
let user: User
|
||||
let creation: Contribution | void
|
||||
let result: any
|
||||
|
||||
describe('AdminResolver', () => {
|
||||
describe('set user role', () => {
|
||||
@ -96,8 +98,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -121,8 +123,8 @@ describe('AdminResolver', () => {
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
admin = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -249,8 +251,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -274,8 +276,8 @@ describe('AdminResolver', () => {
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
admin = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -357,8 +359,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -382,8 +384,8 @@ describe('AdminResolver', () => {
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
admin = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -469,8 +471,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -514,8 +516,8 @@ describe('AdminResolver', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
admin = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
@ -766,8 +768,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -875,8 +877,8 @@ describe('AdminResolver', () => {
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
admin = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -1126,7 +1128,9 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Could not find user with email: bob@baumeister.de')],
|
||||
errors: [
|
||||
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -1200,7 +1204,8 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
|
||||
describe('creation update is not valid', () => {
|
||||
it('throws an error', async () => {
|
||||
// as this test has not clearly defined that date, it is a false positive
|
||||
it.skip('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
@ -1225,7 +1230,8 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
|
||||
describe('creation update is successful changing month', () => {
|
||||
it('returns update creation object', async () => {
|
||||
// skipped as changing the month is currently disable
|
||||
it.skip('returns update creation object', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
@ -1253,7 +1259,8 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
|
||||
describe('creation update is successful without changing month', () => {
|
||||
it('returns update creation object', async () => {
|
||||
// actually this mutation IS changing the month
|
||||
it.skip('returns update creation object', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminUpdateContribution,
|
||||
@ -1297,10 +1304,10 @@ describe('AdminResolver', () => {
|
||||
lastName: 'Lustig',
|
||||
email: 'peter@lustig.de',
|
||||
date: expect.any(String),
|
||||
memo: 'Das war leider zu Viel!',
|
||||
amount: '200',
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
amount: '400',
|
||||
moderator: admin.id,
|
||||
creation: ['1000', '1000', '300'],
|
||||
creation: ['1000', '600', '500'],
|
||||
},
|
||||
{
|
||||
id: expect.any(Number),
|
||||
@ -1311,7 +1318,7 @@ describe('AdminResolver', () => {
|
||||
memo: 'Grundeinkommen',
|
||||
amount: '500',
|
||||
moderator: admin.id,
|
||||
creation: ['1000', '1000', '300'],
|
||||
creation: ['1000', '600', '500'],
|
||||
},
|
||||
{
|
||||
id: expect.any(Number),
|
||||
@ -1360,6 +1367,38 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin deletes own user contribution', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
result = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminDeleteContribution,
|
||||
variables: {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Own contribution can not be deleted as admin')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation id does exist', () => {
|
||||
it('returns true', async () => {
|
||||
await expect(
|
||||
@ -1516,6 +1555,7 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
await expect(r2).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
// data: { confirmContribution: true },
|
||||
errors: [new GraphQLError('Creation was not successful.')],
|
||||
}),
|
||||
)
|
||||
@ -1553,8 +1593,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -1599,8 +1639,8 @@ describe('AdminResolver', () => {
|
||||
}
|
||||
|
||||
// admin: only now log in
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -1789,13 +1829,14 @@ describe('AdminResolver', () => {
|
||||
})
|
||||
|
||||
describe('Contribution Links', () => {
|
||||
const now = new Date()
|
||||
const variables = {
|
||||
amount: new Decimal(200),
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
|
||||
cycle: 'once',
|
||||
validFrom: new Date(2022, 5, 18).toISOString(),
|
||||
validTo: new Date(2022, 7, 14).toISOString(),
|
||||
validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
}
|
||||
@ -1859,8 +1900,8 @@ describe('AdminResolver', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -1933,8 +1974,8 @@ describe('AdminResolver', () => {
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -1977,7 +2018,7 @@ describe('AdminResolver', () => {
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Danke für deine Teilnahme an der Dokumenta 2022',
|
||||
validFrom: new Date('2022-06-18T00:00:00.000Z'),
|
||||
validTo: new Date('2022-08-14T00:00:00.000Z'),
|
||||
validTo: expect.any(Date),
|
||||
cycle: 'once',
|
||||
maxPerCycle: 1,
|
||||
totalMaxCountOfContribution: null,
|
||||
@ -1987,8 +2028,8 @@ describe('AdminResolver', () => {
|
||||
deletedAt: null,
|
||||
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
||||
linkEnabled: true,
|
||||
// amount: '200',
|
||||
// maxAmountPerMonth: '200',
|
||||
amount: expect.decimalEqual(200),
|
||||
maxAmountPerMonth: expect.decimalEqual(200),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -2277,7 +2318,7 @@ describe('AdminResolver', () => {
|
||||
id: linkId,
|
||||
name: 'Dokumenta 2023',
|
||||
memo: 'Danke für deine Teilnahme an der Dokumenta 2023',
|
||||
// amount: '400',
|
||||
amount: expect.decimalEqual(400),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -4,8 +4,6 @@ import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type
|
||||
import {
|
||||
getCustomRepository,
|
||||
IsNull,
|
||||
Not,
|
||||
ObjectLiteral,
|
||||
getConnection,
|
||||
In,
|
||||
MoreThan,
|
||||
@ -17,6 +15,7 @@ import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkList } from '@model/ContributionLinkList'
|
||||
import { Contribution } from '@model/Contribution'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { UserRepository } from '@repository/User'
|
||||
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
||||
@ -25,14 +24,11 @@ import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionRepository } from '@repository/Transaction'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { User } from '@model/User'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
@ -43,8 +39,7 @@ import { Decay } from '@model/Decay'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||
import { Order } from '@enum/Order'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import CONFIG from '@/config'
|
||||
@ -62,12 +57,14 @@ import {
|
||||
MEMO_MAX_CHARS,
|
||||
MEMO_MIN_CHARS,
|
||||
} from './const/const'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
||||
import { ContributionMessageType } from '@enum/MessageType'
|
||||
import { ContributionMessage } from '@model/ContributionMessage'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
|
||||
import { ContributionListResult } from '../model/Contribution'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
@ -81,24 +78,12 @@ export class AdminResolver {
|
||||
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||
): Promise<SearchUsersResult> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
const filterCriteria: ObjectLiteral[] = []
|
||||
if (filters) {
|
||||
if (filters.byActivated !== null) {
|
||||
filterCriteria.push({ emailChecked: filters.byActivated })
|
||||
}
|
||||
|
||||
if (filters.byDeleted !== null) {
|
||||
filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||
}
|
||||
}
|
||||
|
||||
const userFields = [
|
||||
'id',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'email',
|
||||
'emailChecked',
|
||||
'emailId',
|
||||
'emailContact',
|
||||
'deletedAt',
|
||||
'isAdmin',
|
||||
]
|
||||
@ -107,7 +92,7 @@ export class AdminResolver {
|
||||
return 'user.' + fieldName
|
||||
}),
|
||||
searchText,
|
||||
filterCriteria,
|
||||
filters,
|
||||
currentPage,
|
||||
pageSize,
|
||||
)
|
||||
@ -124,32 +109,18 @@ export class AdminResolver {
|
||||
const adminUsers = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
let emailConfirmationSend = ''
|
||||
if (!user.emailChecked) {
|
||||
const emailOptIn = await LoginEmailOptIn.findOne(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
order: {
|
||||
updatedAt: 'DESC',
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
select: ['updatedAt', 'createdAt'],
|
||||
},
|
||||
)
|
||||
if (emailOptIn) {
|
||||
if (emailOptIn.updatedAt) {
|
||||
emailConfirmationSend = emailOptIn.updatedAt.toISOString()
|
||||
} else {
|
||||
emailConfirmationSend = emailOptIn.createdAt.toISOString()
|
||||
}
|
||||
if (!user.emailContact.emailChecked) {
|
||||
if (user.emailContact.updatedAt) {
|
||||
emailConfirmationSend = user.emailContact.updatedAt.toISOString()
|
||||
} else {
|
||||
emailConfirmationSend = user.emailContact.createdAt.toISOString()
|
||||
}
|
||||
}
|
||||
const userCreations = creations.find((c) => c.id === user.id)
|
||||
const adminUser = new UserAdmin(
|
||||
user,
|
||||
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
||||
await hasElopageBuys(user.email),
|
||||
await hasElopageBuys(user.emailContact.email),
|
||||
emailConfirmationSend,
|
||||
)
|
||||
return adminUser
|
||||
@ -245,24 +216,39 @@ export class AdminResolver {
|
||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Decimal[]> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
logger.info(
|
||||
`adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`,
|
||||
)
|
||||
const emailContact = await UserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find user with email: ${email}`)
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
if (emailContact.deletedAt) {
|
||||
logger.error('This emailContact was deleted. Cannot create a contribution.')
|
||||
throw new Error('This emailContact was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (emailContact.user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot create a contribution.')
|
||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (!user.emailChecked) {
|
||||
if (!emailContact.emailChecked) {
|
||||
logger.error('Contribution could not be saved, Email is not activated')
|
||||
throw new Error('Contribution could not be saved, Email is not activated')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
logger.trace('moderator: ', moderator.id)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creations = await getUserCreation(emailContact.userId)
|
||||
logger.trace('creations:', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
logger.trace('creationDateObj:', creationDateObj)
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
const contribution = DbContribution.create()
|
||||
contribution.userId = emailContact.userId
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
@ -272,8 +258,8 @@ export class AdminResolver {
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
return getUserCreation(user.id)
|
||||
await DbContribution.save(contribution)
|
||||
return getUserCreation(emailContact.userId)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||
@ -309,29 +295,43 @@ export class AdminResolver {
|
||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<AdminUpdateContribution> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
const emailContact = await UserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
}
|
||||
const user = emailContact.user
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
logger.error(`Could not find User to emailContact: ${email}`)
|
||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
logger.error(`User was deleted (${email})`)
|
||||
throw new Error(`User was deleted (${email})`)
|
||||
}
|
||||
|
||||
const moderator = getUser(context)
|
||||
|
||||
const contributionToUpdate = await Contribution.findOne({
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
where: { id, confirmedAt: IsNull() },
|
||||
})
|
||||
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id.')
|
||||
throw new Error('No contribution found to given id.')
|
||||
}
|
||||
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
logger.error('user of the pending contribution and send user does not correspond')
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
if (contributionToUpdate.moderatorId === null) {
|
||||
logger.error('An admin is not allowed to update a user contribution.')
|
||||
throw new Error('An admin is not allowed to update a user contribution.')
|
||||
}
|
||||
|
||||
@ -339,6 +339,9 @@ export class AdminResolver {
|
||||
let creations = await getUserCreation(user.id)
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate)
|
||||
} else {
|
||||
logger.error('Currently the month of the contribution cannot change.')
|
||||
throw new Error('Currently the month of the contribution cannot change.')
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
@ -349,7 +352,7 @@ export class AdminResolver {
|
||||
contributionToUpdate.moderatorId = moderator.id
|
||||
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
await Contribution.save(contributionToUpdate)
|
||||
await DbContribution.save(contributionToUpdate)
|
||||
const result = new AdminUpdateContribution()
|
||||
result.amount = amount
|
||||
result.memo = contributionToUpdate.memo
|
||||
@ -366,7 +369,7 @@ export class AdminResolver {
|
||||
const contributions = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('c')
|
||||
.from(Contribution, 'c')
|
||||
.from(DbContribution, 'c')
|
||||
.leftJoinAndSelect('c.messages', 'm')
|
||||
.where({ confirmedAt: IsNull() })
|
||||
.getMany()
|
||||
@ -377,7 +380,11 @@ export class AdminResolver {
|
||||
|
||||
const userIds = contributions.map((p) => p.userId)
|
||||
const userCreations = await getUserCreations(userIds)
|
||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
||||
const users = await dbUser.find({
|
||||
where: { id: In(userIds) },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
|
||||
return contributions.map((contribution) => {
|
||||
const user = users.find((u) => u.id === contribution.userId)
|
||||
@ -393,12 +400,24 @@ export class AdminResolver {
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||
const contribution = await Contribution.findOne(id)
|
||||
async adminDeleteContribution(
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
if (
|
||||
contribution.contributionType === ContributionType.USER &&
|
||||
contribution.userId === moderator.id
|
||||
) {
|
||||
throw new Error('Own contribution can not be deleted as admin')
|
||||
}
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
contribution.deletedBy = moderator.id
|
||||
await contribution.save()
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
@ -410,17 +429,24 @@ export class AdminResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const contribution = await Contribution.findOne(id)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found to given id.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
logger.error('Moderator can not confirm own contribution')
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
|
||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
|
||||
}
|
||||
const user = await dbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
if (user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
}
|
||||
const creations = await getUserCreation(contribution.userId, false)
|
||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||
|
||||
@ -428,7 +454,7 @@ export class AdminResolver {
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||
try {
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
@ -468,7 +494,7 @@ export class AdminResolver {
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution)
|
||||
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
@ -477,7 +503,7 @@ export class AdminResolver {
|
||||
senderLastName: moderatorUser.lastName,
|
||||
recipientFirstName: user.firstName,
|
||||
recipientLastName: user.lastName,
|
||||
recipientEmail: user.email,
|
||||
recipientEmail: user.emailContact.email,
|
||||
contributionMemo: contribution.memo,
|
||||
contributionAmount: contribution.amount,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
@ -493,56 +519,64 @@ export class AdminResolver {
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
|
||||
@Query(() => [Transaction])
|
||||
@Query(() => ContributionListResult)
|
||||
async creationTransactionList(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||
@Arg('userId', () => Int) userId: number,
|
||||
): Promise<Transaction[]> {
|
||||
): Promise<ContributionListResult> {
|
||||
const offset = (currentPage - 1) * pageSize
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions] = await transactionRepository.findByUserPaged(
|
||||
userId,
|
||||
pageSize,
|
||||
offset,
|
||||
order,
|
||||
true,
|
||||
)
|
||||
const [contributionResult, count] = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('c')
|
||||
.from(DbContribution, 'c')
|
||||
.leftJoinAndSelect('c.user', 'u')
|
||||
.where(`user_id = ${userId}`)
|
||||
.limit(pageSize)
|
||||
.offset(offset)
|
||||
.orderBy('c.created_at', order)
|
||||
.getManyAndCount()
|
||||
|
||||
const user = await dbUser.findOneOrFail({ id: userId })
|
||||
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
contributionResult.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||
)
|
||||
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
|
||||
@Mutation(() => Boolean)
|
||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await dbUser.findOneOrFail({ email: email })
|
||||
|
||||
// can be both types: REGISTER and RESET_PASSWORD
|
||||
let optInCode = await LoginEmailOptIn.findOne({
|
||||
where: { userId: user.id },
|
||||
order: { updatedAt: 'DESC' },
|
||||
})
|
||||
|
||||
optInCode = await checkOptInCode(optInCode, user)
|
||||
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||
const user = await findUserByEmail(email)
|
||||
if (!user) {
|
||||
logger.error(`Could not find User to emailContact: ${email}`)
|
||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
logger.error(`User with emailContact: ${email} is deleted.`)
|
||||
throw new Error(`User with emailContact: ${email} is deleted.`)
|
||||
}
|
||||
const emailContact = user.emailContact
|
||||
if (emailContact.deletedAt) {
|
||||
logger.error(`The emailContact: ${email} of htis User is deleted.`)
|
||||
throw new Error(`The emailContact: ${email} of htis User is deleted.`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountActivationEmail({
|
||||
link: activationLink(optInCode),
|
||||
link: activationLink(emailContact.emailVerificationCode),
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
|
||||
/* uncomment this, when you need the activation link on the console
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Account confirmation link: ${activationLink}`)
|
||||
logger.info(`Account confirmation link: ${activationLink}`)
|
||||
}
|
||||
*/
|
||||
|
||||
return true
|
||||
}
|
||||
@ -655,6 +689,7 @@ export class AdminResolver {
|
||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||
): Promise<ContributionLinkList> {
|
||||
const [links, count] = await DbContributionLink.findAndCount({
|
||||
where: [{ validTo: MoreThan(new Date()) }, { validTo: IsNull() }],
|
||||
order: { createdAt: order },
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
@ -720,12 +755,15 @@ export class AdminResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionMessage> {
|
||||
const user = getUser(context)
|
||||
if (!user.emailContact) {
|
||||
user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } })
|
||||
}
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
const contributionMessage = DbContributionMessage.create()
|
||||
try {
|
||||
const contribution = await Contribution.findOne({
|
||||
const contribution = await DbContribution.findOne({
|
||||
where: { id: contributionId },
|
||||
relations: ['user'],
|
||||
})
|
||||
@ -735,6 +773,11 @@ export class AdminResolver {
|
||||
if (contribution.userId === user.id) {
|
||||
throw new Error('Admin can not answer on own contribution')
|
||||
}
|
||||
if (!contribution.user.emailContact) {
|
||||
contribution.user.emailContact = await UserContact.findOneOrFail({
|
||||
where: { id: contribution.user.emailId },
|
||||
})
|
||||
}
|
||||
contributionMessage.contributionId = contributionId
|
||||
contributionMessage.createdAt = new Date()
|
||||
contributionMessage.message = message
|
||||
@ -749,21 +792,21 @@ export class AdminResolver {
|
||||
contribution.contributionStatus === ContributionStatus.PENDING
|
||||
) {
|
||||
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
||||
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
|
||||
await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
|
||||
}
|
||||
await queryRunner.commitTransaction()
|
||||
|
||||
await sendAddedContributionMessageEmail({
|
||||
senderFirstName: user.firstName,
|
||||
senderLastName: user.lastName,
|
||||
recipientFirstName: contribution.user.firstName,
|
||||
recipientLastName: contribution.user.lastName,
|
||||
recipientEmail: contribution.user.email,
|
||||
senderEmail: user.email,
|
||||
recipientEmail: contribution.user.emailContact.email,
|
||||
senderEmail: user.emailContact.email,
|
||||
contributionMemo: contribution.memo,
|
||||
message,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
})
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
||||
|
||||
@ -7,8 +7,9 @@ import {
|
||||
adminCreateContributionMessage,
|
||||
createContribution,
|
||||
createContributionMessage,
|
||||
login,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listContributionMessages, login } from '@/seeds/graphql/queries'
|
||||
import { listContributionMessages } from '@/seeds/graphql/queries'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
@ -21,14 +22,13 @@ jest.mock('@/mailer/sendAddedContributionMessageEmail', () => {
|
||||
}
|
||||
})
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let mutate: any, con: any
|
||||
let testEnv: any
|
||||
let result: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
@ -59,8 +59,8 @@ describe('ContributionMessageResolver', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
result = await mutate({
|
||||
@ -71,8 +71,8 @@ describe('ContributionMessageResolver', () => {
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -103,8 +103,8 @@ describe('ContributionMessageResolver', () => {
|
||||
})
|
||||
|
||||
it('throws error when contribution.userId equals user.id', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
const result2 = await mutate({
|
||||
@ -195,8 +195,8 @@ describe('ContributionMessageResolver', () => {
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -227,8 +227,8 @@ describe('ContributionMessageResolver', () => {
|
||||
})
|
||||
|
||||
it('throws error when other user tries to send createContributionMessage', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
@ -253,8 +253,8 @@ describe('ContributionMessageResolver', () => {
|
||||
|
||||
describe('valid input', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -304,8 +304,8 @@ describe('ContributionMessageResolver', () => {
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,7 +23,7 @@ export class ContributionMessageResolver {
|
||||
const user = getUser(context)
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
const contributionMessage = DbContributionMessage.create()
|
||||
try {
|
||||
const contribution = await Contribution.findOne({ id: contributionId })
|
||||
|
||||
@ -8,14 +8,18 @@ import {
|
||||
createContribution,
|
||||
deleteContribution,
|
||||
updateContribution,
|
||||
login,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listAllContributions, listContributions, login } from '@/seeds/graphql/queries'
|
||||
import { listAllContributions, listContributions } from '@/seeds/graphql/queries'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { creationFactory } from '@/seeds/factory/creation'
|
||||
import { creations } from '@/seeds/creation/index'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
@ -35,6 +39,8 @@ afterAll(async () => {
|
||||
})
|
||||
|
||||
describe('ContributionResolver', () => {
|
||||
let bibi: any
|
||||
|
||||
describe('createContribution', () => {
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
@ -54,8 +60,9 @@ describe('ContributionResolver', () => {
|
||||
describe('authenticated with valid user', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
|
||||
bibi = await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -84,6 +91,10 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < (5)`)
|
||||
})
|
||||
|
||||
it('throws error when memo length greater than 255 chars', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
@ -102,6 +113,10 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > (255)`)
|
||||
})
|
||||
|
||||
it('throws error when creationDate not-valid', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
@ -121,6 +136,13 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
'Invalid Date',
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when creationDate 3 month behind', async () => {
|
||||
const date = new Date()
|
||||
await expect(
|
||||
@ -140,20 +162,31 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
'Invalid Date',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
let contribution: any
|
||||
|
||||
beforeAll(async () => {
|
||||
contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('creates contribution', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect(contribution).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
createContribution: {
|
||||
@ -165,6 +198,17 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the create contribution event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.CONTRIBUTION_CREATE,
|
||||
amount: expect.decimalEqual(100),
|
||||
contributionId: contribution.data.createContribution.id,
|
||||
userId: bibi.data.login.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -197,8 +241,8 @@ describe('ContributionResolver', () => {
|
||||
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
@ -310,8 +354,8 @@ describe('ContributionResolver', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
result = await mutate({
|
||||
@ -347,6 +391,10 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('No contribution found to given id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length smaller than 5 chars', () => {
|
||||
@ -368,6 +416,10 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < (5)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length greater than 255 chars', () => {
|
||||
@ -389,12 +441,16 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > (255)')
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrong user tries to update the contribution', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -420,6 +476,12 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('admin tries to update a user contribution', () => {
|
||||
@ -441,12 +503,14 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// TODO check that the error is logged (need to modify AdminResolver, avoid conflicts)
|
||||
})
|
||||
|
||||
describe('update too much so that the limit is exceeded', () => {
|
||||
beforeAll(async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
@ -472,6 +536,12 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount (1019 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update creation to a date that is older than 3 months', () => {
|
||||
@ -489,12 +559,17 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('No information for available creations for the given date'),
|
||||
],
|
||||
errors: [new GraphQLError('Currently the month of the contribution cannot change.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
'Invalid Date',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
@ -521,6 +596,22 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the update contribution event in the database', async () => {
|
||||
bibi = await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.CONTRIBUTION_UPDATE,
|
||||
amount: expect.decimalEqual(10),
|
||||
contributionId: result.data.createContribution.id,
|
||||
userId: bibi.data.login.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -553,8 +644,8 @@ describe('ContributionResolver', () => {
|
||||
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
@ -627,11 +718,13 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
let peter: any
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
peter = await userFactory(testEnv, peterLustig)
|
||||
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
result = await mutate({
|
||||
@ -664,12 +757,16 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found for given id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('other user sends a deleteContribtuion', () => {
|
||||
describe('other user sends a deleteContribution', () => {
|
||||
it('returns an error', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
@ -685,6 +782,10 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('Can not delete contribution of another user')
|
||||
})
|
||||
})
|
||||
|
||||
describe('User deletes own contribution', () => {
|
||||
@ -698,12 +799,39 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toBeTruthy()
|
||||
})
|
||||
|
||||
it('stores the delete contribution event in the database', async () => {
|
||||
const contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
amount: 166.0,
|
||||
memo: 'Whatever contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
})
|
||||
|
||||
await mutate({
|
||||
mutation: deleteContribution,
|
||||
variables: {
|
||||
id: contribution.data.createContribution.id,
|
||||
},
|
||||
})
|
||||
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.CONTRIBUTION_DELETE,
|
||||
contributionId: contribution.data.createContribution.id,
|
||||
amount: expect.decimalEqual(166),
|
||||
userId: peter.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User deletes already confirmed contribution', () => {
|
||||
it('throws an error', async () => {
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
@ -712,8 +840,8 @@ describe('ContributionResolver', () => {
|
||||
id: result.data.createContribution.id,
|
||||
},
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
await expect(
|
||||
@ -729,6 +857,10 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,6 +13,13 @@ import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import {
|
||||
Event,
|
||||
EventContributionCreate,
|
||||
EventContributionDelete,
|
||||
EventContributionUpdate,
|
||||
} from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@ -23,15 +30,17 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS})`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS})`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
@ -49,6 +58,13 @@ export class ContributionResolver {
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
await dbContribution.save(contribution)
|
||||
|
||||
const eventCreateContribution = new EventContributionCreate()
|
||||
eventCreateContribution.userId = user.id
|
||||
eventCreateContribution.amount = amount
|
||||
eventCreateContribution.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution))
|
||||
|
||||
return new UnconfirmedContribution(contribution, user, creations)
|
||||
}
|
||||
|
||||
@ -58,19 +74,33 @@ export class ContributionResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const event = new Event()
|
||||
const user = getUser(context)
|
||||
const contribution = await dbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error('Contribution not found for given id')
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
if (contribution.userId !== user.id) {
|
||||
logger.error('Can not delete contribution of another user')
|
||||
throw new Error('Can not delete contribution of another user')
|
||||
}
|
||||
if (contribution.confirmedAt) {
|
||||
logger.error('A confirmed contribution can not be deleted')
|
||||
throw new Error('A confirmed contribution can not be deleted')
|
||||
}
|
||||
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
contribution.deletedBy = user.id
|
||||
contribution.deletedAt = new Date()
|
||||
await contribution.save()
|
||||
|
||||
const eventDeleteContribution = new EventContributionDelete()
|
||||
eventDeleteContribution.userId = user.id
|
||||
eventDeleteContribution.contributionId = contribution.id
|
||||
eventDeleteContribution.amount = contribution.amount
|
||||
await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution))
|
||||
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
}
|
||||
@ -98,6 +128,7 @@ export class ContributionResolver {
|
||||
.from(dbContribution, 'c')
|
||||
.leftJoinAndSelect('c.messages', 'm')
|
||||
.where(where)
|
||||
.withDeleted()
|
||||
.orderBy('c.createdAt', order)
|
||||
.limit(pageSize)
|
||||
.offset((currentPage - 1) * pageSize)
|
||||
@ -154,9 +185,11 @@ export class ContributionResolver {
|
||||
where: { id: contributionId, confirmedAt: IsNull() },
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id')
|
||||
throw new Error('No contribution found to given id.')
|
||||
}
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
logger.error('user of the pending contribution and send user does not correspond')
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
@ -164,6 +197,9 @@ export class ContributionResolver {
|
||||
let creations = await getUserCreation(user.id)
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate)
|
||||
} else {
|
||||
logger.error('Currently the month of the contribution cannot change.')
|
||||
throw new Error('Currently the month of the contribution cannot change.')
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
@ -174,6 +210,14 @@ export class ContributionResolver {
|
||||
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
|
||||
dbContribution.save(contributionToUpdate)
|
||||
|
||||
const event = new Event()
|
||||
|
||||
const eventUpdateContribution = new EventContributionUpdate()
|
||||
eventUpdateContribution.userId = user.id
|
||||
eventUpdateContribution.contributionId = contributionId
|
||||
eventUpdateContribution.amount = amount
|
||||
await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
|
||||
|
||||
return new UnconfirmedContribution(contributionToUpdate, user, creations)
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ export class GdtResolver {
|
||||
|
||||
try {
|
||||
const resultGDT = await apiGet(
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
||||
)
|
||||
if (!resultGDT.success) {
|
||||
throw new Error(resultGDT.data)
|
||||
@ -37,7 +37,7 @@ export class GdtResolver {
|
||||
const user = getUser(context)
|
||||
try {
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: user.email,
|
||||
email: user.emailContact.email,
|
||||
})
|
||||
if (!resultGDTSum.success) {
|
||||
throw new Error('Call not successful')
|
||||
|
||||
@ -1,4 +1,168 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { transactionLinkCode } from './TransactionLinkResolver'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { cleanDB, testEnvironment } from '@test/helpers'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { login, createContributionLink, redeemTransactionLink } from '@/seeds/graphql/mutations'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { GraphQLError } from 'graphql'
|
||||
|
||||
let mutate: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('TransactionLinkResolver', () => {
|
||||
describe('redeem daily Contribution Link', () => {
|
||||
const now = new Date()
|
||||
let contributionLink: DbContributionLink | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(5),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('has a daily contribution link in the database', async () => {
|
||||
const cls = await DbContributionLink.find()
|
||||
expect(cls).toHaveLength(1)
|
||||
contributionLink = cls[0]
|
||||
expect(contributionLink).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
validFrom: new Date(now.getFullYear(), 0, 1),
|
||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
|
||||
cycle: 'DAILY',
|
||||
maxPerCycle: 1,
|
||||
totalMaxCountOfContribution: null,
|
||||
maxAccountBalance: null,
|
||||
minGapHours: null,
|
||||
createdAt: expect.any(Date),
|
||||
deletedAt: null,
|
||||
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
||||
linkEnabled: true,
|
||||
amount: expect.decimalEqual(5),
|
||||
maxAmountPerMonth: expect.decimalEqual(200),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('after one day', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
||||
jest.runAllTimers()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('allows the user to redeem the contribution link again', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
data: {
|
||||
redeemTransactionLink: true,
|
||||
},
|
||||
errors: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: {
|
||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transactionLinkCode', () => {
|
||||
const date = new Date()
|
||||
|
||||
@ -34,6 +34,7 @@ import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { Decay } from '@model/Decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
import { ContributionCycleType } from '@enum/ContributionCycleType'
|
||||
|
||||
const QueryLinkResult = createUnionType({
|
||||
name: 'QueryLinkResult', // the name of the GraphQL union
|
||||
@ -73,10 +74,7 @@ export class TransactionLinkResolver {
|
||||
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
|
||||
|
||||
// validate amount
|
||||
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
await calculateBalance(user.id, holdAvailableAmount, createdDate)
|
||||
|
||||
const transactionLink = dbTransactionLink.create()
|
||||
transactionLink.userId = user.id
|
||||
@ -178,7 +176,7 @@ export class TransactionLinkResolver {
|
||||
logger.info('redeem contribution link...')
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('SERIALIZABLE')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
try {
|
||||
const contributionLink = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
@ -204,23 +202,60 @@ export class TransactionLinkResolver {
|
||||
throw new Error('Contribution link is depricated')
|
||||
}
|
||||
}
|
||||
if (contributionLink.cycle !== 'ONCE') {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
}
|
||||
// Test ONCE rule
|
||||
const alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
})
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error('contribution link with rule ONCE already redeemed by user with id', user.id)
|
||||
throw new Error('Contribution link already redeemed')
|
||||
let alreadyRedeemed: DbContribution | undefined
|
||||
switch (contributionLink.cycle) {
|
||||
case ContributionCycleType.ONCE: {
|
||||
alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where('contribution.contributionLinkId = :linkId AND contribution.userId = :id', {
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
})
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error(
|
||||
'contribution link with rule ONCE already redeemed by user with id',
|
||||
user.id,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed')
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContributionCycleType.DAILY: {
|
||||
const start = new Date()
|
||||
start.setHours(0, 0, 0, 0)
|
||||
const end = new Date()
|
||||
end.setHours(23, 59, 59, 999)
|
||||
alreadyRedeemed = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('contribution')
|
||||
.from(DbContribution, 'contribution')
|
||||
.where(
|
||||
`contribution.contributionLinkId = :linkId AND contribution.userId = :id
|
||||
AND Date(contribution.confirmedAt) BETWEEN :start AND :end`,
|
||||
{
|
||||
linkId: contributionLink.id,
|
||||
id: user.id,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
)
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error(
|
||||
'contribution link with rule DAILY already redeemed by user with id',
|
||||
user.id,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed today')
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
}
|
||||
}
|
||||
|
||||
const creations = await getUserCreation(user.id, false)
|
||||
@ -283,7 +318,10 @@ export class TransactionLinkResolver {
|
||||
return true
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
const linkedUser = await dbUser.findOneOrFail(
|
||||
{ id: transactionLink.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
|
||||
366
backend/src/graphql/resolver/TransactionResolver.test.ts
Normal file
366
backend/src/graphql/resolver/TransactionResolver.test.ts
Normal file
@ -0,0 +1,366 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import {
|
||||
confirmContribution,
|
||||
createContribution,
|
||||
login,
|
||||
sendCoins,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { garrickOllivander } from '@/seeds/users/garrick-ollivander'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { stephenHawking } from '@/seeds/users/stephen-hawking'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { User } from '@entity/User'
|
||||
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||
import { logger } from '@test/testSetup'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment(logger)
|
||||
mutate = testEnv.mutate
|
||||
query = testEnv.query
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
let bobData: any
|
||||
let peterData: any
|
||||
let user: User[]
|
||||
|
||||
describe('send coins', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
await userFactory(testEnv, stephenHawking)
|
||||
await userFactory(testEnv, garrickOllivander)
|
||||
|
||||
bobData = {
|
||||
email: 'bob@baumeister.de',
|
||||
password: 'Aa12345_',
|
||||
}
|
||||
|
||||
peterData = {
|
||||
email: 'peter@lustig.de',
|
||||
password: 'Aa12345_',
|
||||
}
|
||||
|
||||
user = await User.find({ relations: ['emailContact'] })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
describe('unknown recipient', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: bobData,
|
||||
})
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'wrong@email.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('No user with this credentials')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`UserContact with email=wrong@email.com does not exists`)
|
||||
})
|
||||
|
||||
describe('deleted recipient', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: peterData,
|
||||
})
|
||||
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'stephen@hawking.uk',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account was deleted')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail(peterData.email)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The recipient account was deleted: recipientUser=${user}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('recipient account not activated', () => {
|
||||
it('throws an error', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: peterData,
|
||||
})
|
||||
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'garrick@ollivander.com',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The recipient account is not activated')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail(peterData.email)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The recipient account is not activated: recipientUser=${user}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors in the transaction itself', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: bobData,
|
||||
})
|
||||
})
|
||||
|
||||
describe('sender and recipient are the same', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Sender and Recipient are the same.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Sender and Recipient are the same.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too long', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=256 > 255')
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too short', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has not enough GDD', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'testing',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`User has not received any GDD yet`)],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`No prior transaction found for user with id: ${user[1].id}`,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sending negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: -50,
|
||||
memo: 'testing negative',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Transaction amount must be greater than 0')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Transaction amount must be greater than 0: -50')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has some GDD', () => {
|
||||
beforeAll(async () => {
|
||||
resetToken()
|
||||
|
||||
// login as bob again
|
||||
await query({ mutation: login, variables: bobData })
|
||||
|
||||
// create contribution as user bob
|
||||
const contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() },
|
||||
})
|
||||
|
||||
// login as admin
|
||||
await query({ mutation: login, variables: peterData })
|
||||
|
||||
// confirm the contribution
|
||||
await mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: { id: contribution.data.createContribution.id },
|
||||
})
|
||||
|
||||
// login as bob again
|
||||
await query({ mutation: login, variables: bobData })
|
||||
})
|
||||
|
||||
describe('good transaction', () => {
|
||||
it('sends the coins', async () => {
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 50,
|
||||
memo: 'unrepeatable memo',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
sendCoins: 'true',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the send transaction 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',
|
||||
})
|
||||
|
||||
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.TRANSACTION_SEND,
|
||||
userId: user[1].id,
|
||||
transactionId: transaction[0].id,
|
||||
xUserId: user[0].id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the 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',
|
||||
})
|
||||
|
||||
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.TRANSACTION_RECEIVE,
|
||||
userId: user[0].id,
|
||||
transactionId: transaction[0].id,
|
||||
xUserId: user[1].id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -6,12 +6,9 @@ import CONFIG from '@/config'
|
||||
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql'
|
||||
import { getCustomRepository, getConnection } from '@dbTools/typeorm'
|
||||
import { getCustomRepository, getConnection, In } from '@dbTools/typeorm'
|
||||
|
||||
import {
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
} from '@/mailer/sendTransactionReceivedEmail'
|
||||
import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail'
|
||||
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionList } from '@model/TransactionList'
|
||||
@ -38,6 +35,11 @@ import Decimal from 'decimal.js-light'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import { Decay } from '../model/Decay'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -56,32 +58,23 @@ export const executeTransaction = async (
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > (${MEMO_MAX_CHARS}`)
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < (${MEMO_MIN_CHARS}`)
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
sender.id,
|
||||
amount.mul(-1),
|
||||
receivedCallDate,
|
||||
transactionLink,
|
||||
)
|
||||
logger.debug(`calculated Balance=${sendBalance}`)
|
||||
if (!sendBalance) {
|
||||
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
const sendBalance = await calculateBalance(sender.id, amount, receivedCallDate, transactionLink)
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
logger.debug(`open Transaction to write...`)
|
||||
try {
|
||||
// transaction
|
||||
@ -107,7 +100,24 @@ export const executeTransaction = async (
|
||||
transactionReceive.userId = recipient.id
|
||||
transactionReceive.linkedUserId = sender.id
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
|
||||
// state received balance
|
||||
let receiveBalance: {
|
||||
balance: Decimal
|
||||
decay: Decay
|
||||
lastTransactionId: number
|
||||
} | null
|
||||
|
||||
// try received balance
|
||||
try {
|
||||
receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`User with no transactions sent: ${recipient.id}, has received a transaction of ${amount} GDD from user: ${sender.id}`,
|
||||
)
|
||||
receiveBalance = null
|
||||
}
|
||||
|
||||
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
|
||||
@ -136,6 +146,20 @@ export const executeTransaction = async (
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info(`commit Transaction successful...`)
|
||||
|
||||
const eventTransactionSend = new EventTransactionSend()
|
||||
eventTransactionSend.userId = transactionSend.userId
|
||||
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
||||
eventTransactionSend.transactionId = transactionSend.id
|
||||
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
||||
|
||||
const eventTransactionReceive = new EventTransactionReceive()
|
||||
eventTransactionReceive.userId = transactionReceive.userId
|
||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||
eventTransactionReceive.transactionId = transactionReceive.id
|
||||
eventTransactionReceive.amount = transactionReceive.amount
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Transaction was not successful: ${e}`)
|
||||
@ -151,10 +175,9 @@ export const executeTransaction = async (
|
||||
senderLastName: sender.lastName,
|
||||
recipientFirstName: recipient.firstName,
|
||||
recipientLastName: recipient.lastName,
|
||||
email: recipient.email,
|
||||
senderEmail: sender.email,
|
||||
email: recipient.emailContact.email,
|
||||
senderEmail: sender.emailContact.email,
|
||||
amount,
|
||||
memo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
})
|
||||
if (transactionLink) {
|
||||
@ -163,8 +186,8 @@ export const executeTransaction = async (
|
||||
senderLastName: recipient.lastName,
|
||||
recipientFirstName: sender.firstName,
|
||||
recipientLastName: sender.lastName,
|
||||
email: sender.email,
|
||||
senderEmail: recipient.email,
|
||||
email: sender.emailContact.email,
|
||||
senderEmail: recipient.emailContact.email,
|
||||
amount,
|
||||
memo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
@ -187,7 +210,7 @@ export class TransactionResolver {
|
||||
const user = getUser(context)
|
||||
|
||||
logger.addContext('user', user.id)
|
||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
|
||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
@ -226,11 +249,11 @@ export class TransactionResolver {
|
||||
logger.debug(`involvedUserIds=${involvedUserIds}`)
|
||||
|
||||
// We need to show the name for deleted users for old transactions
|
||||
const involvedDbUsers = await dbUser
|
||||
.createQueryBuilder()
|
||||
.withDeleted()
|
||||
.where('id IN (:...userIds)', { userIds: involvedUserIds })
|
||||
.getMany()
|
||||
const involvedDbUsers = await dbUser.find({
|
||||
where: { id: In(involvedUserIds) },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
const involvedUsers = involvedDbUsers.map((u) => new User(u))
|
||||
logger.debug(`involvedUsers=${involvedUsers}`)
|
||||
|
||||
@ -309,16 +332,29 @@ export class TransactionResolver {
|
||||
}
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
/*
|
||||
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
}
|
||||
*/
|
||||
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||
|
||||
/* Code inside this if statement is unreachable (useless by so),
|
||||
in findUserByEmail() an error is already thrown if the user is not found
|
||||
*/
|
||||
if (!recipientUser) {
|
||||
logger.error(`recipient not known: email=${email}`)
|
||||
throw new Error('recipient not known')
|
||||
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||
throw new Error('unknown recipient')
|
||||
}
|
||||
if (recipientUser.deletedAt) {
|
||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account was deleted')
|
||||
}
|
||||
if (!recipientUser.emailChecked) {
|
||||
const emailContact = recipientUser.emailContact
|
||||
if (!emailContact.emailChecked) {
|
||||
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account is not activated')
|
||||
}
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers'
|
||||
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
||||
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||
import {
|
||||
login,
|
||||
logout,
|
||||
createUser,
|
||||
setPassword,
|
||||
forgotPassword,
|
||||
updateUserInfos,
|
||||
createContribution,
|
||||
confirmContribution,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
@ -15,15 +23,19 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi
|
||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||
import { printTimeDuration, activationLink } from './UserResolver'
|
||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
// import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { logger } from '@test/testSetup'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { OptInType } from '../enum/OptInType'
|
||||
import { UserContactType } from '../enum/UserContactType'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
|
||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
|
||||
@ -84,7 +96,7 @@ describe('UserResolver', () => {
|
||||
}
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let emailVerificationCode: string
|
||||
let user: User[]
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -103,11 +115,11 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
// let loginEmailOptIn: LoginEmailOptIn[]
|
||||
beforeAll(async () => {
|
||||
user = await User.find()
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
user = await User.find({ relations: ['emailContact'] })
|
||||
// loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailVerificationCode = user[0].emailContact.emailVerificationCode.toString()
|
||||
})
|
||||
|
||||
describe('filling all tables', () => {
|
||||
@ -117,15 +129,16 @@ describe('UserResolver', () => {
|
||||
id: expect.any(Number),
|
||||
gradidoID: expect.any(String),
|
||||
alias: null,
|
||||
email: 'peter@lustig.de',
|
||||
emailContact: expect.any(UserContact), // 'peter@lustig.de',
|
||||
emailId: expect.any(Number),
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
password: '0',
|
||||
pubKey: null,
|
||||
privKey: null,
|
||||
emailHash: expect.any(Buffer),
|
||||
// emailHash: expect.any(Buffer),
|
||||
createdAt: expect.any(Date),
|
||||
emailChecked: false,
|
||||
// emailChecked: false,
|
||||
passphrase: expect.any(String),
|
||||
language: 'de',
|
||||
isAdmin: null,
|
||||
@ -141,18 +154,21 @@ describe('UserResolver', () => {
|
||||
expect(verUUID).toEqual(4)
|
||||
})
|
||||
|
||||
it('creates an email optin', () => {
|
||||
expect(loginEmailOptIn).toEqual([
|
||||
{
|
||||
id: expect.any(Number),
|
||||
userId: user[0].id,
|
||||
verificationCode: expect.any(String),
|
||||
emailOptInTypeId: 1,
|
||||
createdAt: expect.any(Date),
|
||||
resendCount: 0,
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
])
|
||||
it('creates an email contact', () => {
|
||||
expect(user[0].emailContact).toEqual({
|
||||
id: expect.any(Number),
|
||||
type: UserContactType.USER_CONTACT_EMAIL,
|
||||
userId: user[0].id,
|
||||
email: 'peter@lustig.de',
|
||||
emailChecked: false,
|
||||
emailVerificationCode: expect.any(String),
|
||||
emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER,
|
||||
emailResendCount: 0,
|
||||
phone: null,
|
||||
createdAt: expect.any(Date),
|
||||
deletedAt: null,
|
||||
updatedAt: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -161,7 +177,7 @@ describe('UserResolver', () => {
|
||||
it('sends an account activation email', () => {
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
emailOptIn,
|
||||
emailVerificationCode,
|
||||
).replace(/{code}/g, '')
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
link: activationLink,
|
||||
@ -219,13 +235,13 @@ describe('UserResolver', () => {
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
language: 'de',
|
||||
}),
|
||||
]),
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
user: expect.objectContaining({ language: 'de' }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -236,10 +252,12 @@ describe('UserResolver', () => {
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
publisherId: null,
|
||||
}),
|
||||
]),
|
||||
@ -248,13 +266,15 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('redeem codes', () => {
|
||||
let result: any
|
||||
let link: ContributionLink
|
||||
|
||||
describe('contribution link', () => {
|
||||
let link: ContributionLink
|
||||
beforeAll(async () => {
|
||||
// activate account of admin Peter Lustig
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
// make Peter Lustig Admin
|
||||
@ -277,16 +297,24 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
resetToken()
|
||||
await mutate({
|
||||
result = await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
it('sets the contribution link id', async () => {
|
||||
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
contributionLinkId: link.id,
|
||||
user: expect.objectContaining({
|
||||
contributionLinkId: link.id,
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -299,6 +327,99 @@ describe('UserResolver', () => {
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the redeem register event in the database', () => {
|
||||
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.REDEEM_REGISTER,
|
||||
userId: result.data.createUser.id,
|
||||
contributionId: link.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction link', () => {
|
||||
let contribution: any
|
||||
let bob: any
|
||||
let transactionLink: TransactionLink
|
||||
let newUser: any
|
||||
|
||||
const bobData = {
|
||||
email: 'bob@baumeister.de',
|
||||
password: 'Aa12345_',
|
||||
publisherId: 1234,
|
||||
}
|
||||
|
||||
const peterData = {
|
||||
email: 'peter@lustig.de',
|
||||
password: 'Aa12345_',
|
||||
publisherId: 1234,
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
await mutate({ mutation: login, variables: bobData })
|
||||
|
||||
// create contribution as user bob
|
||||
contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() },
|
||||
})
|
||||
|
||||
// login as admin
|
||||
await mutate({ mutation: login, variables: peterData })
|
||||
|
||||
// confirm the contribution
|
||||
contribution = await mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: { id: contribution.data.createContribution.id },
|
||||
})
|
||||
|
||||
// login as user bob
|
||||
bob = await mutate({ mutation: login, variables: bobData })
|
||||
|
||||
// create transaction link
|
||||
await transactionLinkFactory(testEnv, {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 19.99,
|
||||
memo: `testing transaction link`,
|
||||
})
|
||||
|
||||
transactionLink = await TransactionLink.findOneOrFail()
|
||||
|
||||
resetToken()
|
||||
|
||||
// create new user using transaction link of bob
|
||||
newUser = await mutate({
|
||||
mutation: createUser,
|
||||
variables: {
|
||||
...variables,
|
||||
email: 'which@ever.de',
|
||||
redeemCode: transactionLink.code,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('sets the referrer id to bob baumeister id', async () => {
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
user: expect.objectContaining({ referrerId: bob.data.login.id }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the redeem register event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.REDEEM_REGISTER,
|
||||
userId: newUser.data.createUser.id,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
/* A transaction link requires GDD on account
|
||||
@ -310,7 +431,7 @@ describe('UserResolver', () => {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 19.99,
|
||||
memo: `Kein Trick, keine Zauberrei,
|
||||
bei Gradidio sei dabei!`,
|
||||
bei Gradidio sei dabei!`,
|
||||
})
|
||||
const transactionLink = await TransactionLink.findOneOrFail()
|
||||
resetToken()
|
||||
@ -319,14 +440,14 @@ bei Gradidio sei dabei!`,
|
||||
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||
referrerId: user[0].id,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
*/
|
||||
})
|
||||
})
|
||||
@ -341,20 +462,23 @@ bei Gradidio sei dabei!`,
|
||||
}
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let emailVerificationCode: string
|
||||
|
||||
describe('valid optin code and valid password', () => {
|
||||
let newUser: any
|
||||
let newUser: User
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||
})
|
||||
newUser = await User.find()
|
||||
newUser = await User.findOneOrFail(
|
||||
{ id: emailContact.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -362,11 +486,11 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
it('sets email checked to true', () => {
|
||||
expect(newUser[0].emailChecked).toBeTruthy()
|
||||
expect(newUser.emailContact.emailChecked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('updates the password', () => {
|
||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
||||
expect(newUser.password).toEqual('3917921995996627700')
|
||||
})
|
||||
|
||||
/*
|
||||
@ -388,11 +512,11 @@ bei Gradidio sei dabei!`,
|
||||
describe('no valid password', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'not-valid' },
|
||||
variables: { code: emailVerificationCode, password: 'not-valid' },
|
||||
})
|
||||
})
|
||||
|
||||
@ -459,7 +583,8 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
describe('no users in database', () => {
|
||||
beforeAll(async () => {
|
||||
result = await query({ query: login, variables })
|
||||
jest.clearAllMocks()
|
||||
result = await mutate({ mutation: login, variables })
|
||||
})
|
||||
|
||||
it('throws an error', () => {
|
||||
@ -471,14 +596,16 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('User with email=bibi@bloxberg.de does not exist')
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'UserContact with email=bibi@bloxberg.de does not exists',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user is in database and correct login data', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
result = await query({ query: login, variables })
|
||||
result = await mutate({ mutation: login, variables })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -515,7 +642,7 @@ bei Gradidio sei dabei!`,
|
||||
describe('user is in database and wrong password', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
result = await query({ query: login, variables: { ...variables, password: 'wrong' } })
|
||||
result = await mutate({ mutation: login, variables: { ...variables, password: 'wrong' } })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -540,7 +667,7 @@ bei Gradidio sei dabei!`,
|
||||
describe('unauthenticated', () => {
|
||||
it('throws an error', async () => {
|
||||
resetToken()
|
||||
await expect(query({ query: logout })).resolves.toEqual(
|
||||
await expect(mutate({ mutation: logout })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
}),
|
||||
@ -556,7 +683,7 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({ query: login, variables })
|
||||
await mutate({ mutation: login, variables })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -564,7 +691,7 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
await expect(query({ query: logout })).resolves.toEqual(
|
||||
await expect(mutate({ mutation: logout })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: { logout: 'true' },
|
||||
errors: undefined,
|
||||
@ -613,7 +740,7 @@ bei Gradidio sei dabei!`,
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await query({ query: login, variables })
|
||||
await mutate({ mutation: login, variables })
|
||||
user = await User.find()
|
||||
})
|
||||
|
||||
@ -656,46 +783,68 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
describe('forgotPassword', () => {
|
||||
const variables = { email: 'bibi@bloxberg.de' }
|
||||
const emailCodeRequestTime = CONFIG.EMAIL_CODE_REQUEST_TIME
|
||||
|
||||
describe('user is not in DB', () => {
|
||||
it('returns true', async () => {
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
describe('duration not expired', () => {
|
||||
it('returns true', async () => {
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user exists in DB', () => {
|
||||
let result: any
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
let emailContact: UserContact
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await resetEntity(LoginEmailOptIn)
|
||||
result = await mutate({ mutation: forgotPassword, variables })
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
// await resetEntity(LoginEmailOptIn)
|
||||
emailContact = await UserContact.findOneOrFail(variables)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
await expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
describe('duration not expired', () => {
|
||||
it('returns true', async () => {
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('duration reset to 0', () => {
|
||||
it('returns true', async () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = 0
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('sends reset password email', () => {
|
||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||
link: activationLink(loginEmailOptIn[0]),
|
||||
link: activationLink(emailContact.emailVerificationCode),
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
@ -704,7 +853,8 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
describe('request reset password again', () => {
|
||||
it('throws an error', async () => {
|
||||
it('thows an error', async () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||
@ -720,11 +870,11 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
describe('queryOptIn', () => {
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
let emailContact: UserContact
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -739,8 +889,8 @@ bei Gradidio sei dabei!`,
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
// keep Whitspace in error message!
|
||||
new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
|
||||
"verificationCode": "not-valid"
|
||||
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
|
||||
"emailVerificationCode": "not-valid"
|
||||
}`),
|
||||
],
|
||||
}),
|
||||
@ -753,7 +903,7 @@ bei Gradidio sei dabei!`,
|
||||
await expect(
|
||||
query({
|
||||
query: queryOptIn,
|
||||
variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
|
||||
variables: { optIn: emailContact.emailVerificationCode.toString() },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
@ -781,8 +931,8 @@ bei Gradidio sei dabei!`,
|
||||
describe('authenticated', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
password: 'Aa12345_',
|
||||
@ -913,8 +1063,8 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
it('can login with new password', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: login,
|
||||
mutate({
|
||||
mutation: login,
|
||||
variables: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
password: 'Bb12345_',
|
||||
@ -933,8 +1083,8 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
it('cannot login with old password', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: login,
|
||||
mutate({
|
||||
mutation: login,
|
||||
variables: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
password: 'Aa12345_',
|
||||
@ -971,8 +1121,8 @@ bei Gradidio sei dabei!`,
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await query({
|
||||
query: login,
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: {
|
||||
email: 'bibi@bloxberg.de',
|
||||
password: 'Aa12345_',
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import fs from 'fs'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { User } from '@model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
@ -16,7 +16,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
@ -29,10 +28,12 @@ import {
|
||||
EventLogin,
|
||||
EventRedeemRegister,
|
||||
EventRegister,
|
||||
EventSendAccountMultiRegistrationEmail,
|
||||
EventSendConfirmationEmail,
|
||||
EventActivateAccount,
|
||||
} from '@/event/Event'
|
||||
import { getUserCreation } from './util/creations'
|
||||
import { UserContactType } from '../enum/UserContactType'
|
||||
import { UserRepository } from '@/typeorm/repository/User'
|
||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
import Paginated from '@arg/Paginated'
|
||||
@ -147,6 +148,7 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
|
||||
return [encryptionKeyHash, encryptionKey]
|
||||
}
|
||||
|
||||
/*
|
||||
const getEmailHash = (email: string): Buffer => {
|
||||
logger.trace('getEmailHash...')
|
||||
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
||||
@ -154,6 +156,7 @@ const getEmailHash = (email: string): Buffer => {
|
||||
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
||||
return emailHash
|
||||
}
|
||||
*/
|
||||
|
||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||
logger.trace('SecretKeyCryptographyEncrypt...')
|
||||
@ -178,6 +181,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
||||
return message
|
||||
}
|
||||
|
||||
const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
logger.trace(`newEmailContact...`)
|
||||
const emailContact = new DbUserContact()
|
||||
emailContact.email = email
|
||||
emailContact.userId = userId
|
||||
emailContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
logger.debug(`newEmailContact...successful: ${emailContact}`)
|
||||
return emailContact
|
||||
}
|
||||
/*
|
||||
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||
logger.trace('newEmailOptIn...')
|
||||
const emailOptIn = new LoginEmailOptIn()
|
||||
@ -187,7 +203,8 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||
return emailOptIn
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
// needed by AdminResolver
|
||||
// checks if given code exists and can be resent
|
||||
// if optIn does not exits, it is created
|
||||
@ -227,10 +244,44 @@ export const checkOptInCode = async (
|
||||
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
||||
return optInCode
|
||||
}
|
||||
*/
|
||||
export const checkEmailVerificationCode = async (
|
||||
emailContact: DbUserContact,
|
||||
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||
): Promise<DbUserContact> => {
|
||||
logger.info(`checkEmailVerificationCode... ${emailContact}`)
|
||||
if (emailContact.updatedAt) {
|
||||
if (!canEmailResend(emailContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
throw new Error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
}
|
||||
emailContact.updatedAt = new Date()
|
||||
emailContact.emailResendCount++
|
||||
} else {
|
||||
logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId)
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
}
|
||||
emailContact.emailOptInTypeId = optInType
|
||||
await DbUserContact.save(emailContact).catch(() => {
|
||||
logger.error('Unable to save email verification code= ' + emailContact)
|
||||
throw new Error('Unable to save email verification code.')
|
||||
})
|
||||
logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`)
|
||||
return emailContact
|
||||
}
|
||||
|
||||
export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
||||
logger.debug(`activationLink(${LoginEmailOptIn})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
||||
export const activationLink = (verificationCode: BigInt): string => {
|
||||
logger.debug(`activationLink(${verificationCode})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||
}
|
||||
|
||||
const newGradidoID = async (): Promise<string> => {
|
||||
@ -265,7 +316,7 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LOGIN])
|
||||
@Query(() => User)
|
||||
@Mutation(() => User)
|
||||
@UseMiddleware(klicktippNewsletterStateMiddleware)
|
||||
async login(
|
||||
@Args() { email, password, publisherId }: UnsecureLoginArgs,
|
||||
@ -273,15 +324,12 @@ export class UserResolver {
|
||||
): Promise<User> {
|
||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||
email = email.trim().toLowerCase()
|
||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
||||
logger.error(`User with email=${email} does not exist`)
|
||||
throw new Error('No user with this credentials')
|
||||
})
|
||||
const dbUser = await findUserByEmail(email)
|
||||
if (dbUser.deletedAt) {
|
||||
logger.error('The User was permanently deleted in database.')
|
||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
||||
}
|
||||
if (!dbUser.emailChecked) {
|
||||
if (!dbUser.emailContact.emailChecked) {
|
||||
logger.error('The Users email is not validate yet.')
|
||||
throw new Error('User email not validated')
|
||||
}
|
||||
@ -303,10 +351,10 @@ export class UserResolver {
|
||||
}
|
||||
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
|
||||
logger.addContext('user', dbUser.id)
|
||||
logger.debug('login credentials valid...')
|
||||
logger.debug('validation of login credentials successful...')
|
||||
|
||||
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
||||
logger.debug('user=' + user)
|
||||
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||
@ -324,12 +372,12 @@ export class UserResolver {
|
||||
const ev = new EventLogin()
|
||||
ev.userId = user.id
|
||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||
logger.info('successful Login:' + user)
|
||||
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||
return user
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.LOGOUT])
|
||||
@Query(() => String)
|
||||
@Mutation(() => String)
|
||||
async logout(): Promise<boolean> {
|
||||
// TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token.
|
||||
// Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login)
|
||||
@ -348,71 +396,78 @@ export class UserResolver {
|
||||
@Args()
|
||||
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
|
||||
): Promise<User> {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.info(
|
||||
`createUser(email=${email}, firstName=${firstName}, lastName=${lastName}, language=${language}, publisherId=${publisherId}, redeemCode =${redeemCode})`,
|
||||
)
|
||||
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||
// default int publisher_id = 0;
|
||||
const event = new Event()
|
||||
|
||||
// Validate Language (no throw)
|
||||
if (!language || !isLanguage(language)) {
|
||||
language = DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
// Validate email unique
|
||||
// check if user with email still exists?
|
||||
email = email.trim().toLowerCase()
|
||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||
if (await checkEmailExists(email)) {
|
||||
const foundUser = await findUserByEmail(email)
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||
|
||||
if (userFound) {
|
||||
// ATTENTION: this logger-message will be exactly expected during tests
|
||||
logger.info(`User already exists with this email=${email}`)
|
||||
// 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.
|
||||
if (foundUser) {
|
||||
// ATTENTION: this logger-message will be exactly expected during tests
|
||||
logger.info(`User already exists with this email=${email}`)
|
||||
// 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.gradidoID = uuidv4()
|
||||
user.email = email
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
user.publisherId = publisherId
|
||||
logger.debug('partly faked user=' + user)
|
||||
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.gradidoID = uuidv4()
|
||||
user.email = email
|
||||
user.firstName = firstName
|
||||
user.lastName = lastName
|
||||
user.language = language
|
||||
user.publisherId = publisherId
|
||||
logger.debug('partly faked user=' + user)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
})
|
||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Email not sent!`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||
firstName,
|
||||
lastName,
|
||||
email,
|
||||
})
|
||||
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||
eventProtocol.writeEvent(
|
||||
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||
)
|
||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Email not send!`)
|
||||
}
|
||||
logger.info('createUser() faked and send multi registration mail...')
|
||||
|
||||
return user
|
||||
}
|
||||
logger.info('createUser() faked and send multi registration mail...')
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
const passphrase = PassphraseGenerate()
|
||||
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
const emailHash = getEmailHash(email)
|
||||
// const emailHash = getEmailHash(email)
|
||||
const gradidoID = await newGradidoID()
|
||||
|
||||
const eventRegister = new EventRegister()
|
||||
const eventRedeemRegister = new EventRedeemRegister()
|
||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||
const dbUser = new DbUser()
|
||||
|
||||
let dbUser = new DbUser()
|
||||
dbUser.gradidoID = gradidoID
|
||||
dbUser.email = email
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.emailHash = emailHash
|
||||
dbUser.language = language
|
||||
dbUser.publisherId = publisherId
|
||||
dbUser.passphrase = passphrase.join(' ')
|
||||
@ -443,25 +498,38 @@ export class UserResolver {
|
||||
// loginUser.pubKey = keyPair[0]
|
||||
// loginUser.privKey = encryptedPrivkey
|
||||
|
||||
const event = new Event()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
try {
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
logger.error('Error while saving dbUser', error)
|
||||
throw new Error('error saving user')
|
||||
})
|
||||
let emailContact = newEmailContact(email, dbUser.id)
|
||||
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
|
||||
logger.error('Error while saving emailContact', error)
|
||||
throw new Error('error saving email user contact')
|
||||
})
|
||||
|
||||
dbUser.emailContact = emailContact
|
||||
dbUser.emailId = emailContact.id
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
logger.error('Error while updating dbUser', error)
|
||||
throw new Error('error updating user')
|
||||
})
|
||||
|
||||
/*
|
||||
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||
logger.error('Error while saving emailOptIn', error)
|
||||
throw new Error('error saving email opt in')
|
||||
})
|
||||
*/
|
||||
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
emailOptIn.verificationCode.toString(),
|
||||
emailContact.emailVerificationCode.toString(),
|
||||
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -476,13 +544,12 @@ export class UserResolver {
|
||||
eventSendConfirmEmail.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
||||
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.addContext('user', dbUser.id)
|
||||
} catch (e) {
|
||||
logger.error(`error during create user with ${e}`)
|
||||
await queryRunner.rollbackTransaction()
|
||||
@ -494,10 +561,10 @@ export class UserResolver {
|
||||
|
||||
if (redeemCode) {
|
||||
eventRedeemRegister.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||
} else {
|
||||
eventRegister.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||
}
|
||||
|
||||
return new User(dbUser)
|
||||
@ -506,24 +573,32 @@ export class UserResolver {
|
||||
@Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL])
|
||||
@Mutation(() => Boolean)
|
||||
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.info(`forgotPassword(${email})...`)
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await DbUser.findOne({ email })
|
||||
const user = await findUserByEmail(email).catch(() => {
|
||||
logger.warn(`fail on find UserContact per ${email}`)
|
||||
})
|
||||
if (!user) {
|
||||
logger.warn(`no user found with ${email}`)
|
||||
return true
|
||||
}
|
||||
|
||||
// can be both types: REGISTER and RESET_PASSWORD
|
||||
let optInCode = await LoginEmailOptIn.findOne({
|
||||
userId: user.id,
|
||||
})
|
||||
// let optInCode = await LoginEmailOptIn.findOne({
|
||||
// userId: user.id,
|
||||
// })
|
||||
// let optInCode = user.emailContact.emailVerificationCode
|
||||
const dbUserContact = await checkEmailVerificationCode(
|
||||
user.emailContact,
|
||||
OptInType.EMAIL_OPT_IN_RESET_PASSWORD,
|
||||
)
|
||||
|
||||
optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||
logger.info(`optInCode for ${email}=${optInCode}`)
|
||||
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||
logger.info(`optInCode for ${email}=${dbUserContact}`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendResetPasswordEmailMailer({
|
||||
link: activationLink(optInCode),
|
||||
link: activationLink(dbUserContact.emailVerificationCode),
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
@ -533,7 +608,7 @@ export class UserResolver {
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Reset password link: ${activationLink(optInCode)}`)
|
||||
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
|
||||
}
|
||||
logger.info(`forgotPassword(${email}) successful...`)
|
||||
|
||||
@ -556,13 +631,22 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
// Load code
|
||||
/*
|
||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||
logger.error('Could not login with emailVerificationCode')
|
||||
throw new Error('Could not login with emailVerificationCode')
|
||||
})
|
||||
logger.debug('optInCode loaded...')
|
||||
*/
|
||||
const userContact = await DbUserContact.findOneOrFail(
|
||||
{ emailVerificationCode: code },
|
||||
{ relations: ['user'] },
|
||||
).catch(() => {
|
||||
logger.error('Could not login with emailVerificationCode')
|
||||
throw new Error('Could not login with emailVerificationCode')
|
||||
})
|
||||
logger.debug('userContact loaded...')
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isOptInValid(optInCode)) {
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
@ -570,14 +654,11 @@ export class UserResolver {
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
}
|
||||
logger.debug('optInCode is valid...')
|
||||
logger.debug('EmailVerificationCode is valid...')
|
||||
|
||||
// load user
|
||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
||||
logger.error('Could not find corresponding Login User')
|
||||
throw new Error('Could not find corresponding Login User')
|
||||
})
|
||||
logger.debug('user with optInCode found...')
|
||||
const user = userContact.user
|
||||
logger.debug('user with EmailVerificationCode found...')
|
||||
|
||||
// Generate Passphrase if needed
|
||||
if (!user.passphrase) {
|
||||
@ -597,10 +678,10 @@ export class UserResolver {
|
||||
logger.debug('Passphrase is valid...')
|
||||
|
||||
// Activate EMail
|
||||
user.emailChecked = true
|
||||
userContact.emailChecked = true
|
||||
|
||||
// Update Password
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
|
||||
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||
@ -610,7 +691,7 @@ export class UserResolver {
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
const event = new Event()
|
||||
|
||||
@ -620,17 +701,21 @@ export class UserResolver {
|
||||
logger.error('error saving user: ' + error)
|
||||
throw new Error('error saving user: ' + error)
|
||||
})
|
||||
// Save userContact
|
||||
await queryRunner.manager.save(userContact).catch((error) => {
|
||||
logger.error('error saving userContact: ' + error)
|
||||
throw new Error('error saving userContact: ' + error)
|
||||
})
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('User and UserContact data written successfully...')
|
||||
|
||||
const eventActivateAccount = new EventActivateAccount()
|
||||
eventActivateAccount.userId = user.id
|
||||
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
||||
|
||||
logger.info('User data written successfully...')
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error('Error on writing User data:' + e)
|
||||
logger.error('Error on writing User and UserContact data:' + e)
|
||||
throw e
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
@ -638,11 +723,11 @@ export class UserResolver {
|
||||
|
||||
// Sign into Klicktipp
|
||||
// TODO do we always signUp the user? How to handle things with old users?
|
||||
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||
try {
|
||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
||||
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
|
||||
logger.debug(
|
||||
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Error subscribe to klicktipp:' + e)
|
||||
@ -661,10 +746,10 @@ export class UserResolver {
|
||||
@Query(() => Boolean)
|
||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||
logger.info(`queryOptIn(${optIn})...`)
|
||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
||||
logger.debug(`found optInCode=${optInCode}`)
|
||||
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||
logger.debug(`found optInCode=${userContact}`)
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isOptInValid(optInCode)) {
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
@ -712,7 +797,10 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(
|
||||
userEntity.emailContact.email,
|
||||
password,
|
||||
)
|
||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||
logger.error(`Old password is invalid`)
|
||||
throw new Error(`Old password is invalid`)
|
||||
@ -720,7 +808,10 @@ export class UserResolver {
|
||||
|
||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||
logger.debug('oldPassword decrypted...')
|
||||
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
||||
const newPasswordHash = SecretKeyCryptographyCreateKey(
|
||||
userEntity.emailContact.email,
|
||||
passwordNew,
|
||||
) // return short and long hash
|
||||
logger.debug('newPasswordHash created...')
|
||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||
logger.debug('PrivateKey encrypted...')
|
||||
@ -732,7 +823,7 @@ export class UserResolver {
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
try {
|
||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||
@ -757,12 +848,8 @@ export class UserResolver {
|
||||
@Query(() => Boolean)
|
||||
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
||||
logger.info(`hasElopage()...`)
|
||||
const userEntity = context.user
|
||||
if (!userEntity) {
|
||||
logger.info('missing context.user for EloPage-check')
|
||||
return false
|
||||
}
|
||||
const elopageBuys = hasElopageBuys(userEntity.email)
|
||||
const userEntity = getUser(context)
|
||||
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
|
||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||
return elopageBuys
|
||||
}
|
||||
@ -798,19 +885,58 @@ export class UserResolver {
|
||||
}
|
||||
}
|
||||
|
||||
export async function findUserByEmail(email: string): Promise<DbUser> {
|
||||
const dbUserContact = await DbUserContact.findOneOrFail(
|
||||
{ email: email },
|
||||
{ withDeleted: true, relations: ['user'] },
|
||||
).catch(() => {
|
||||
logger.error(`UserContact with email=${email} does not exists`)
|
||||
throw new Error('No user with this credentials')
|
||||
})
|
||||
const dbUser = dbUserContact.user
|
||||
dbUser.emailContact = dbUserContact
|
||||
return dbUser
|
||||
}
|
||||
|
||||
async function checkEmailExists(email: string): Promise<boolean> {
|
||||
const userContact = await DbUserContact.findOne({ email: email }, { withDeleted: true })
|
||||
if (userContact) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
||||
// time is given in minutes
|
||||
return timeElapsed <= duration * 60 * 1000
|
||||
}
|
||||
|
||||
*/
|
||||
const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
|
||||
const timeElapsed = Date.now() - new Date(updatedAt).getTime()
|
||||
// time is given in minutes
|
||||
return timeElapsed <= duration * 60 * 1000
|
||||
}
|
||||
/*
|
||||
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||
}
|
||||
|
||||
*/
|
||||
const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => {
|
||||
if (updatedAt == null) {
|
||||
return true
|
||||
}
|
||||
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||
}
|
||||
/*
|
||||
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||
}
|
||||
*/
|
||||
const canEmailResend = (updatedAt: Date): boolean => {
|
||||
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||
}
|
||||
|
||||
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||
if (time > 60) {
|
||||
|
||||
@ -14,14 +14,21 @@ export const validateContribution = (
|
||||
amount: Decimal,
|
||||
creationDate: Date,
|
||||
): void => {
|
||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||
logger.trace('isContributionValid: ', creations, amount, creationDate)
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
logger.error(
|
||||
'No information for available creations with the given creationDate=',
|
||||
creationDate.toString(),
|
||||
)
|
||||
throw new Error('No information for available creations for the given date')
|
||||
}
|
||||
|
||||
if (amount.greaterThan(creations[index].toString())) {
|
||||
logger.error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
@ -40,7 +47,7 @@ export const getUserCreations = async (
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||
logger.trace('getUserCreations dateFilter=', dateFilter)
|
||||
|
||||
/**
|
||||
SELECT MONTH(contribution_date) as month, user_id, created_at, sum(amount), confirmed_at, deleted_at
|
||||
@ -69,6 +76,30 @@ export const getUserCreations = async (
|
||||
const sumAmountContributionPerUserAndLast3Month =
|
||||
await sumAmountContributionPerUserAndLast3MonthQuery.getRawMany()
|
||||
|
||||
/*
|
||||
const unionString = includePending
|
||||
? `
|
||||
UNION
|
||||
SELECT contribution_date AS date, amount AS amount, user_id AS userId FROM contributions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND contribution_date >= ${dateFilter}
|
||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||
: ''
|
||||
logger.trace('getUserCreations unionString=', unionString)
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||
(SELECT creation_date AS date, amount AS amount, user_id AS userId FROM transactions
|
||||
WHERE user_id IN (${ids.toString()})
|
||||
AND type_id = ${TransactionTypeId.CREATION}
|
||||
AND creation_date >= ${dateFilter}
|
||||
${unionString}) AS result
|
||||
GROUP BY month, userId
|
||||
ORDER BY date DESC
|
||||
`)
|
||||
logger.trace('getUserCreations unionQuery=', unionQuery)
|
||||
*/
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
return ids.map((id) => {
|
||||
@ -98,6 +129,7 @@ export const getUserCreations = async (
|
||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||
logger.trace('getUserCreation', id, includePending)
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
logger.trace('getUserCreation creations=', creations)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
|
||||
@ -26,7 +26,7 @@ describe('sendAddedContributionMessageEmail', () => {
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||
subject: 'Gradido Frage zur Schöpfung',
|
||||
subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining('Peter Lustig') &&
|
||||
|
||||
@ -29,6 +29,7 @@ describe('sendEMail', () => {
|
||||
let result: boolean
|
||||
describe('config email is false', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
@ -48,6 +49,7 @@ describe('sendEMail', () => {
|
||||
|
||||
describe('config email is true', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
CONFIG.EMAIL = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
@ -84,4 +86,28 @@ describe('sendEMail', () => {
|
||||
expect(result).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('with email EMAIL_TEST_MODUS true', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
CONFIG.EMAIL = true
|
||||
CONFIG.EMAIL_TEST_MODUS = true
|
||||
result = await sendEMail({
|
||||
to: 'receiver@mail.org',
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendMail of transporter with faked to', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: CONFIG.EMAIL_TEST_RECEIVER,
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -19,6 +19,12 @@ export const sendEMail = async (emailDef: {
|
||||
logger.info(`Emails are disabled via config...`)
|
||||
return false
|
||||
}
|
||||
if (CONFIG.EMAIL_TEST_MODUS) {
|
||||
logger.info(
|
||||
`Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||
)
|
||||
emailDef.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||
}
|
||||
const transporter = createTransport({
|
||||
host: CONFIG.EMAIL_SMTP_URL,
|
||||
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
||||
|
||||
44
backend/src/mailer/sendTransactionLinkRedeemed.test.ts
Normal file
44
backend/src/mailer/sendTransactionLinkRedeemed.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendTransactionLinkRedeemedEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendTransactionLinkRedeemedEmail({
|
||||
email: 'bibi@bloxberg.de',
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
senderEmail: 'peter@lustig.de',
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen Dank dass Du dabei bist',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||
subject: 'Gradido-Link wurde eingelöst',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining(
|
||||
'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.',
|
||||
) &&
|
||||
expect.stringContaining('Betrag: 42,00 GDD,') &&
|
||||
expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') &&
|
||||
expect.stringContaining(
|
||||
'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview',
|
||||
) &&
|
||||
expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'),
|
||||
})
|
||||
})
|
||||
})
|
||||
28
backend/src/mailer/sendTransactionLinkRedeemed.ts
Normal file
28
backend/src/mailer/sendTransactionLinkRedeemed.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionLinkRedeemed } from './text/transactionLinkRedeemed'
|
||||
|
||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||
<${data.email}>,
|
||||
subject=${transactionLinkRedeemed.de.subject},
|
||||
text=${transactionLinkRedeemed.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||
subject: transactionLinkRedeemed.de.subject,
|
||||
text: transactionLinkRedeemed.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -19,7 +19,6 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
email: 'peter@lustig.de',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
@ -33,7 +32,6 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
expect.stringContaining('42,00 GDD') &&
|
||||
expect.stringContaining('Bibi Bloxberg') &&
|
||||
expect.stringContaining('(bibi@bloxberg.de)') &&
|
||||
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') &&
|
||||
expect.stringContaining('http://localhost/overview'),
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionLinkRedeemed, transactionReceived } from './text/transactionReceived'
|
||||
import { transactionReceived } from './text/transactionReceived'
|
||||
|
||||
export const sendTransactionReceivedEmail = (data: {
|
||||
senderFirstName: string
|
||||
@ -11,7 +11,6 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
email: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
@ -26,27 +25,3 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
text: transactionReceived.de.text(data),
|
||||
})
|
||||
}
|
||||
|
||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||
<${data.email}>,
|
||||
subject=${transactionLinkRedeemed.de.subject},
|
||||
text=${transactionLinkRedeemed.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||
subject: transactionLinkRedeemed.de.subject,
|
||||
text: transactionLinkRedeemed.de.text(data),
|
||||
})
|
||||
}
|
||||
|
||||
@ -14,9 +14,10 @@ export const contributionConfirmed = {
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
Dein Gradido Schöpfungsantrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${
|
||||
data.senderLastName
|
||||
} bestätigt.
|
||||
Dein eingereichter Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${
|
||||
data.senderFirstName
|
||||
} ${data.senderLastName} bestätigt.
|
||||
|
||||
Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export const contributionMessageReceived = {
|
||||
de: {
|
||||
subject: 'Gradido Frage zur Schöpfung',
|
||||
subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag',
|
||||
text: (data: {
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
@ -14,17 +14,15 @@ export const contributionMessageReceived = {
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||
|
||||
Du hast soeben zu deinem eingereichten Gradido Schöpfungsantrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
|
||||
Die Rückfrage lautet:
|
||||
du hast soeben zu deinem eingereichten Gemeinwohl-Beitrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
|
||||
|
||||
${data.message}
|
||||
Bitte beantworte die Rückfrage in deinem Gradido-Konto im Menü "Gemeinschaft" im Tab "Meine Beiträge zum Gemeinwohl"!
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team
|
||||
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}`,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
|
||||
33
backend/src/mailer/text/transactionLinkRedeemed.ts
Normal file
33
backend/src/mailer/text/transactionLinkRedeemed.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const transactionLinkRedeemed = {
|
||||
de: {
|
||||
subject: 'Gradido-Link wurde eingelöst',
|
||||
text: (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
|
||||
${data.senderFirstName} ${data.senderLastName} (${
|
||||
data.senderEmail
|
||||
}) hat soeben deinen Link eingelöst.
|
||||
|
||||
Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
|
||||
Memo: ${data.memo}
|
||||
|
||||
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
@ -11,7 +11,6 @@ export const transactionReceived = {
|
||||
email: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
@ -19,47 +18,12 @@ export const transactionReceived = {
|
||||
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
||||
data.senderLastName
|
||||
} (${data.senderEmail}) erhalten.
|
||||
${data.senderFirstName} ${data.senderLastName} schreibt:
|
||||
|
||||
${data.memo}
|
||||
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team
|
||||
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}`,
|
||||
},
|
||||
}
|
||||
|
||||
export const transactionLinkRedeemed = {
|
||||
de: {
|
||||
subject: 'Gradido link eingelösst',
|
||||
text: (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
|
||||
${data.senderFirstName} ${data.senderLastName} (${
|
||||
data.senderEmail
|
||||
}) hat soeben deinen Link eingelösst.
|
||||
Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
|
||||
Memo: ${data.memo}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}`,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { createContributionLink } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { login, createContributionLink } from '@/seeds/graphql/mutations'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface'
|
||||
|
||||
@ -8,10 +7,14 @@ export const contributionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
contributionLink: ContributionLinkInterface,
|
||||
): Promise<ContributionLink> => {
|
||||
const { mutate, query } = client
|
||||
const { mutate } = client
|
||||
|
||||
// login as admin
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const user = await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
const variables = {
|
||||
amount: contributionLink.amount,
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { login, adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { User } from '@entity/User'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
|
||||
// import CONFIG from '@/config/index'
|
||||
|
||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
@ -18,30 +18,42 @@ export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution | void> => {
|
||||
const { mutate, query } = client
|
||||
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
|
||||
const { mutate } = client
|
||||
logger.trace('creationFactory...')
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
logger.trace('creationFactory... after login')
|
||||
// TODO it would be nice to have this mutation return the id
|
||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||
logger.trace('creationFactory... after adminCreateContribution')
|
||||
|
||||
const user = await User.findOneOrFail({ where: { email: creation.email } })
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
|
||||
const pendingCreation = await Contribution.findOneOrFail({
|
||||
where: { userId: user.id, amount: creation.amount },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail pendingCreation=',
|
||||
pendingCreation,
|
||||
)
|
||||
if (creation.confirmed) {
|
||||
logger.trace('creationFactory... creation.confirmed=', creation.confirmed)
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
|
||||
logger.trace('creationFactory... after confirmContribution')
|
||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate)
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction)
|
||||
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedCreation.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
@ -52,11 +64,17 @@ export const creationFactory = async (
|
||||
transaction.balanceDate = new Date(
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
logger.trace('creationFactory... before transaction.save transaction=', transaction)
|
||||
await transaction.save()
|
||||
logger.trace(
|
||||
'creationFactory... before confirmedCreation.save confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
await confirmedCreation.save()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.trace('creationFactory... pendingCreation=', pendingCreation)
|
||||
return pendingCreation
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { createTransactionLink } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { login, createTransactionLink } from '@/seeds/graphql/mutations'
|
||||
import { TransactionLinkInterface } from '@/seeds/transactionLink/TransactionLinkInterface'
|
||||
import { transactionLinkExpireDate } from '@/graphql/resolver/TransactionLinkResolver'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
@ -9,10 +8,13 @@ export const transactionLinkFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
transactionLink: TransactionLinkInterface,
|
||||
): Promise<void> => {
|
||||
const { mutate, query } = client
|
||||
const { mutate } = client
|
||||
|
||||
// login
|
||||
await query({ query: login, variables: { email: transactionLink.email, password: 'Aa12345_' } })
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: transactionLink.email, password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
const variables = {
|
||||
amount: transactionLink.amount,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||
import { User } from '@entity/User'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
|
||||
@ -15,17 +14,23 @@ export const userFactory = async (
|
||||
createUser: { id },
|
||||
},
|
||||
} = await mutate({ mutation: createUser, variables: user })
|
||||
// console.log('creatUser:', { id }, { user })
|
||||
// get user from database
|
||||
let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact'] })
|
||||
// console.log('dbUser:', dbUser)
|
||||
|
||||
const emailContact = dbUser.emailContact
|
||||
// console.log('emailContact:', emailContact)
|
||||
|
||||
if (user.emailChecked) {
|
||||
const optin = await LoginEmailOptIn.findOneOrFail({ userId: id })
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { password: 'Aa12345_', code: optin.verificationCode },
|
||||
variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode },
|
||||
})
|
||||
}
|
||||
|
||||
// get user from database
|
||||
const dbUser = await User.findOneOrFail({ id })
|
||||
// get last changes of user from database
|
||||
dbUser = await User.findOneOrFail({ id })
|
||||
|
||||
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
||||
if (user.createdAt) dbUser.createdAt = user.createdAt
|
||||
@ -34,5 +39,8 @@ export const userFactory = async (
|
||||
await dbUser.save()
|
||||
}
|
||||
|
||||
// get last changes of user from database
|
||||
// dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
|
||||
|
||||
return dbUser
|
||||
}
|
||||
|
||||
@ -289,3 +289,33 @@ export const adminCreateContributionMessage = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const redeemTransactionLink = gql`
|
||||
mutation ($code: String!) {
|
||||
redeemTransactionLink(code: $code)
|
||||
}
|
||||
`
|
||||
|
||||
export const login = gql`
|
||||
mutation ($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
language
|
||||
klickTipp {
|
||||
newsletterState
|
||||
}
|
||||
hasElopage
|
||||
publisherId
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const logout = gql`
|
||||
mutation {
|
||||
logout
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,23 +1,5 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const login = gql`
|
||||
query ($email: String!, $password: String!, $publisherId: Int) {
|
||||
login(email: $email, password: $password, publisherId: $publisherId) {
|
||||
id
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
language
|
||||
klickTipp {
|
||||
newsletterState
|
||||
}
|
||||
hasElopage
|
||||
publisherId
|
||||
isAdmin
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const verifyLogin = gql`
|
||||
query {
|
||||
verifyLogin {
|
||||
@ -35,12 +17,6 @@ export const verifyLogin = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const logout = gql`
|
||||
query {
|
||||
logout
|
||||
}
|
||||
`
|
||||
|
||||
export const queryOptIn = gql`
|
||||
query ($optIn: String!) {
|
||||
queryOptIn(optIn: $optIn)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import createServer from '../server/createServer'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
@ -50,11 +51,14 @@ const run = async () => {
|
||||
const seedClient = createTestClient(server.apollo)
|
||||
const { con } = server
|
||||
await cleanDB()
|
||||
logger.info('##seed## clean database successful...')
|
||||
|
||||
// seed the standard users
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
await userFactory(seedClient, users[i])
|
||||
const dbUser = await userFactory(seedClient, users[i])
|
||||
logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`)
|
||||
}
|
||||
logger.info('##seed## seeding all standard users successful...')
|
||||
|
||||
// seed 100 random users
|
||||
for (let i = 0; i < 100; i++) {
|
||||
@ -64,7 +68,9 @@ const run = async () => {
|
||||
email: internet.email(),
|
||||
language: datatype.boolean() ? 'en' : 'de',
|
||||
})
|
||||
logger.info(`##seed## seed ${i}. random user`)
|
||||
}
|
||||
logger.info('##seed## seeding all random users successful...')
|
||||
|
||||
// create GDD
|
||||
for (let i = 0; i < creations.length; i++) {
|
||||
@ -73,16 +79,19 @@ const run = async () => {
|
||||
// eslint-disable-next-line no-empty
|
||||
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
||||
}
|
||||
logger.info('##seed## seeding all creations successful...')
|
||||
|
||||
// create Transaction Links
|
||||
for (let i = 0; i < transactionLinks.length; i++) {
|
||||
await transactionLinkFactory(seedClient, transactionLinks[i])
|
||||
}
|
||||
logger.info('##seed## seeding all transactionLinks successful...')
|
||||
|
||||
// create Contribution Links
|
||||
for (let i = 0; i < contributionLinks.length; i++) {
|
||||
await contributionLinkFactory(seedClient, contributionLinks[i])
|
||||
}
|
||||
logger.info('##seed## seeding all contributionLinks successful...')
|
||||
|
||||
await con.close()
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ export interface Context {
|
||||
setHeaders: { key: string; value: string }[]
|
||||
role?: Role
|
||||
user?: dbUser
|
||||
clientRequestTime?: string
|
||||
// hack to use less DB calls for Balance Resolver
|
||||
lastTransaction?: dbTransaction
|
||||
transactionCount?: number
|
||||
@ -18,14 +19,17 @@ export interface Context {
|
||||
|
||||
const context = (args: ExpressContext): Context => {
|
||||
const authorization = args.req.headers.authorization
|
||||
let token: string | null = null
|
||||
if (authorization) {
|
||||
token = authorization.replace(/^Bearer /, '')
|
||||
}
|
||||
const context = {
|
||||
token,
|
||||
const clientRequestTime = args.req.headers.clientrequesttime
|
||||
const context: Context = {
|
||||
token: null,
|
||||
setHeaders: [],
|
||||
}
|
||||
if (authorization) {
|
||||
context.token = authorization.replace(/^Bearer /, '')
|
||||
}
|
||||
if (clientRequestTime && typeof clientRequestTime === 'string') {
|
||||
context.clientRequestTime = clientRequestTime
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
|
||||
@ -35,6 +35,7 @@ const createServer = async (
|
||||
context: any = serverContext,
|
||||
logger: Logger = apolloLogger,
|
||||
): Promise<ServerDef> => {
|
||||
logger.addContext('user', 'unknown')
|
||||
logger.debug('createServer...')
|
||||
|
||||
// open mysql connection
|
||||
@ -75,6 +76,9 @@ const createServer = async (
|
||||
logger,
|
||||
})
|
||||
apollo.applyMiddleware({ app, path: '/' })
|
||||
logger.info(
|
||||
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||
)
|
||||
logger.debug('createServer...successful')
|
||||
return { apollo, app, con }
|
||||
}
|
||||
|
||||
@ -1,28 +1,39 @@
|
||||
import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm'
|
||||
import { User } from '@entity/User'
|
||||
import SearchUsersFilters from '@/graphql/arg/SearchUsersFilters'
|
||||
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
async findByPubkeyHex(pubkeyHex: string): Promise<User> {
|
||||
return this.createQueryBuilder('user')
|
||||
@EntityRepository(DbUser)
|
||||
export class UserRepository extends Repository<DbUser> {
|
||||
async findByPubkeyHex(pubkeyHex: string): Promise<DbUser> {
|
||||
const dbUser = await this.createQueryBuilder('user')
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
|
||||
.getOneOrFail()
|
||||
/*
|
||||
const dbUser = await this.findOneOrFail(`hex(user.pubKey) = { pubkeyHex }`)
|
||||
const emailContact = await this.query(
|
||||
`SELECT * from user_contacts where id = { dbUser.emailId }`,
|
||||
)
|
||||
dbUser.emailContact = emailContact
|
||||
*/
|
||||
return dbUser
|
||||
}
|
||||
|
||||
async findBySearchCriteriaPagedFiltered(
|
||||
select: string[],
|
||||
searchCriteria: string,
|
||||
filterCriteria: ObjectLiteral[],
|
||||
filters: SearchUsersFilters,
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
): Promise<[User[], number]> {
|
||||
const query = await this.createQueryBuilder('user')
|
||||
): Promise<[DbUser[], number]> {
|
||||
const query = this.createQueryBuilder('user')
|
||||
.select(select)
|
||||
.withDeleted()
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.where(
|
||||
new Brackets((qb) => {
|
||||
qb.where(
|
||||
'user.firstName like :name or user.lastName like :lastName or user.email like :email',
|
||||
'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email',
|
||||
{
|
||||
name: `%${searchCriteria}%`,
|
||||
lastName: `%${searchCriteria}%`,
|
||||
@ -31,9 +42,23 @@ export class UserRepository extends Repository<User> {
|
||||
)
|
||||
}),
|
||||
)
|
||||
/*
|
||||
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
|
||||
.take(pageSize)
|
||||
.skip((currentPage - 1) * pageSize)
|
||||
|
||||
@ -2,22 +2,26 @@
|
||||
|
||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
// import { UserContact as EmailContact } from '@entity/UserContact'
|
||||
import { User } from '@model/User'
|
||||
|
||||
const communityDbUser: dbUser = {
|
||||
id: -1,
|
||||
gradidoID: '11111111-2222-4333-4444-55555555',
|
||||
alias: '',
|
||||
email: 'support@gradido.net',
|
||||
// email: 'support@gradido.net',
|
||||
emailContact: new UserContact(),
|
||||
emailId: -1,
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
pubKey: Buffer.from(''),
|
||||
privKey: Buffer.from(''),
|
||||
deletedAt: null,
|
||||
password: BigInt(0),
|
||||
emailHash: Buffer.from(''),
|
||||
// emailHash: Buffer.from(''),
|
||||
createdAt: new Date(),
|
||||
emailChecked: false,
|
||||
// emailChecked: false,
|
||||
language: '',
|
||||
isAdmin: null,
|
||||
publisherId: 0,
|
||||
|
||||
@ -7,16 +7,16 @@ export async function retrieveNotRegisteredEmails(): Promise<string[]> {
|
||||
if (!con) {
|
||||
throw new Error('No connection to database')
|
||||
}
|
||||
const users = await User.find()
|
||||
const users = await User.find({ relations: ['emailContact'] })
|
||||
const notRegisteredUser = []
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
const user = users[i]
|
||||
try {
|
||||
await getKlickTippUser(user.email)
|
||||
await getKlickTippUser(user.emailContact.email)
|
||||
} catch (err) {
|
||||
notRegisteredUser.push(user.email)
|
||||
notRegisteredUser.push(user.emailContact.email)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`${user.email}`)
|
||||
console.log(`${user.emailContact.email}`)
|
||||
}
|
||||
}
|
||||
await con.close()
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
export const objectValuesToArray = (obj: { [x: string]: string }): Array<string> => {
|
||||
return Object.keys(obj).map(function (key) {
|
||||
return obj[key]
|
||||
})
|
||||
}
|
||||
|
||||
// to improve code readability, as String is needed, it is handled inside this utility function
|
||||
export const decimalAddition = (a: Decimal, b: Decimal): Decimal => {
|
||||
return a.add(b.toString())
|
||||
}
|
||||
|
||||
// to improve code readability, as String is needed, it is handled inside this utility function
|
||||
export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => {
|
||||
return a.minus(b.toString())
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import { Decay } from '@model/Decay'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { decimalSubtraction, decimalAddition } from './utilities'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
function isStringBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
@ -23,14 +25,26 @@ async function calculateBalance(
|
||||
amount: Decimal,
|
||||
time: Date,
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number }> {
|
||||
// negative or empty amount should not be allowed
|
||||
if (amount.lessThanOrEqualTo(0)) {
|
||||
logger.error(`Transaction amount must be greater than 0: ${amount}`)
|
||||
throw new Error('Transaction amount must be greater than 0')
|
||||
}
|
||||
|
||||
// check if user has prior transactions
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||
if (!lastTransaction) return null
|
||||
|
||||
if (!lastTransaction) {
|
||||
logger.error(`No prior transaction found for user with id: ${userId}`)
|
||||
throw new Error('User has not received any GDD yet')
|
||||
}
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
// TODO why we have to use toString() here?
|
||||
const balance = decay.balance.add(amount.toString())
|
||||
// new balance is the old balance minus the amount used
|
||||
const balance = decimalSubtraction(decay.balance, amount)
|
||||
|
||||
const transactionLinkRepository = getCustomRepository(TransactionLinkRepository)
|
||||
const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time)
|
||||
|
||||
@ -38,11 +52,16 @@ async function calculateBalance(
|
||||
// else we cannot redeem links which are more or equal to half of what an account actually owns
|
||||
const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0)
|
||||
|
||||
if (
|
||||
balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0)
|
||||
) {
|
||||
return null
|
||||
const availableBalance = decimalSubtraction(balance, sumHoldAvailableAmount)
|
||||
|
||||
if (decimalAddition(availableBalance, releasedLinkAmount).lessThan(0)) {
|
||||
logger.error(
|
||||
`Not enough funds for a transaction of ${amount} GDD, user with id: ${userId} has only ${balance} GDD available`,
|
||||
)
|
||||
throw new Error('Not enough funds for transaction')
|
||||
}
|
||||
|
||||
logger.debug(`calculated Balance=${balance}`)
|
||||
return { balance, lastTransactionId: lastTransaction.id, decay }
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
|
||||
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
||||
import { UserResolver } from '@/graphql/resolver/UserResolver'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||
|
||||
export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -127,7 +127,8 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
}
|
||||
|
||||
// Do we already have such a user?
|
||||
if ((await dbUser.count({ email })) !== 0) {
|
||||
// if ((await dbUser.count({ email })) !== 0) {
|
||||
if ((await dbUserContact.count({ email })) !== 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Did not create User - already exists with email: ${email}`)
|
||||
return
|
||||
|
||||
33
backend/test/extensions.ts
Normal file
33
backend/test/extensions.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
|
||||
import Decimal from 'decimal.js-light'
|
||||
|
||||
expect.extend({
|
||||
decimalEqual(received, value) {
|
||||
const pass = new Decimal(value).equals(received.toString())
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected ${received} to not equal ${value}`,
|
||||
pass: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected ${received} to equal ${value}`,
|
||||
pass: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
decimalEqual(value: number): R
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Expect extends CustomMatchers {}
|
||||
interface Matchers<R> extends CustomMatchers<R> {}
|
||||
interface InverseAsymmetricMatchers extends CustomMatchers {}
|
||||
}
|
||||
}
|
||||
@ -1668,9 +1668,9 @@ camelcase@^6.2.0:
|
||||
integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
|
||||
|
||||
caniuse-lite@^1.0.30001264:
|
||||
version "1.0.30001325"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001325.tgz"
|
||||
integrity sha512-sB1bZHjseSjDtijV1Hb7PB2Zd58Kyx+n/9EotvZ4Qcz2K3d0lWB8dB4nb8wN/TsOGFq3UuAm0zQZNQ4SoR7TrQ==
|
||||
version "1.0.30001418"
|
||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz"
|
||||
integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==
|
||||
|
||||
chalk@^2.0.0:
|
||||
version "2.4.2"
|
||||
|
||||
126
database/entity/0049-add_user_contacts_table/User.ts
Normal file
126
database/entity/0049-add_user_contacts_table/User.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
import { UserContact } from '../UserContact'
|
||||
|
||||
@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class User extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
name: 'gradido_id',
|
||||
length: 36,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
gradidoID: string
|
||||
|
||||
@Column({
|
||||
name: 'alias',
|
||||
length: 20,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
alias: string
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
|
||||
pubKey: Buffer
|
||||
|
||||
@Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
|
||||
privKey: Buffer
|
||||
|
||||
/*
|
||||
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
email: string
|
||||
*/
|
||||
@OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
|
||||
@JoinColumn({ name: 'email_id' })
|
||||
emailContact: UserContact
|
||||
|
||||
@Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
emailId: number | null
|
||||
|
||||
@Column({
|
||||
name: 'first_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
firstName: string
|
||||
|
||||
@Column({
|
||||
name: 'last_name',
|
||||
length: 255,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
lastName: string
|
||||
|
||||
@DeleteDateColumn()
|
||||
deletedAt: Date | null
|
||||
|
||||
@Column({ type: 'bigint', default: 0, unsigned: true })
|
||||
password: BigInt
|
||||
|
||||
@Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||
createdAt: Date
|
||||
/*
|
||||
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||
emailChecked: boolean
|
||||
*/
|
||||
|
||||
@Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
|
||||
language: string
|
||||
|
||||
@Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
|
||||
isAdmin: Date | null
|
||||
|
||||
@Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
|
||||
referrerId?: number | null
|
||||
|
||||
@Column({
|
||||
name: 'contribution_link_id',
|
||||
type: 'int',
|
||||
unsigned: true,
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
contributionLinkId?: number | null
|
||||
|
||||
@Column({ name: 'publisher_id', default: 0 })
|
||||
publisherId: number
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
name: 'passphrase',
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
nullable: true,
|
||||
default: null,
|
||||
})
|
||||
passphrase: string
|
||||
|
||||
@OneToMany(() => Contribution, (contribution) => contribution.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
contributions?: Contribution[]
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
messages?: ContributionMessage[]
|
||||
|
||||
@OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
userContacts?: UserContact[]
|
||||
}
|
||||
60
database/entity/0049-add_user_contacts_table/UserContact.ts
Normal file
60
database/entity/0049-add_user_contacts_table/UserContact.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
DeleteDateColumn,
|
||||
OneToOne,
|
||||
} from 'typeorm'
|
||||
import { User } from './User'
|
||||
|
||||
@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
|
||||
export class UserContact extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({
|
||||
name: 'type',
|
||||
length: 100,
|
||||
nullable: true,
|
||||
default: null,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
type: string
|
||||
|
||||
@OneToOne(() => User, (user) => user.emailContact)
|
||||
user: User
|
||||
|
||||
@Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
email: string
|
||||
|
||||
@Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true })
|
||||
emailVerificationCode: BigInt
|
||||
|
||||
@Column({ name: 'email_opt_in_type_id' })
|
||||
emailOptInTypeId: number
|
||||
|
||||
@Column({ name: 'email_resend_count' })
|
||||
emailResendCount: number
|
||||
|
||||
// @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||
// emailHash: Buffer
|
||||
|
||||
@Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
|
||||
emailChecked: boolean
|
||||
|
||||
@Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' })
|
||||
phone: string
|
||||
|
||||
@Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' })
|
||||
updatedAt: Date | null
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at', nullable: true })
|
||||
deletedAt: Date | null
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
|
||||
@Entity('event_protocol')
|
||||
export class EventProtocol extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ length: 100, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
type: string
|
||||
|
||||
@Column({ name: 'created_at', type: 'datetime', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'x_user_id', unsigned: true, nullable: true })
|
||||
xUserId: number
|
||||
|
||||
@Column({ name: 'x_community_id', unsigned: true, nullable: true })
|
||||
xCommunityId: number
|
||||
|
||||
@Column({ name: 'transaction_id', unsigned: true, nullable: true })
|
||||
transactionId: number
|
||||
|
||||
@Column({ name: 'contribution_id', unsigned: true, nullable: true })
|
||||
contributionId: number
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: true,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({ name: 'message_id', unsigned: true, nullable: true })
|
||||
messageId: number
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
import Decimal from 'decimal.js-light'
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
DeleteDateColumn,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
} from 'typeorm'
|
||||
import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer'
|
||||
import { User } from '../User'
|
||||
import { ContributionMessage } from '../ContributionMessage'
|
||||
|
||||
@Entity('contributions')
|
||||
export class Contribution extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ unsigned: true, nullable: false, name: 'user_id' })
|
||||
userId: number
|
||||
|
||||
@ManyToOne(() => User, (user) => user.contributions)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User
|
||||
|
||||
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||
createdAt: Date
|
||||
|
||||
@Column({ type: 'datetime', nullable: false, name: 'contribution_date' })
|
||||
contributionDate: Date
|
||||
|
||||
@Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
memo: string
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
precision: 40,
|
||||
scale: 20,
|
||||
nullable: false,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'moderator_id' })
|
||||
moderatorId: number
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'contribution_link_id' })
|
||||
contributionLinkId: number
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'confirmed_by' })
|
||||
confirmedBy: number
|
||||
|
||||
@Column({ nullable: true, name: 'confirmed_at' })
|
||||
confirmedAt: Date
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'denied_by' })
|
||||
deniedBy: number
|
||||
|
||||
@Column({ nullable: true, name: 'denied_at' })
|
||||
deniedAt: Date
|
||||
|
||||
@Column({
|
||||
name: 'contribution_type',
|
||||
length: 12,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
contributionType: string
|
||||
|
||||
@Column({
|
||||
name: 'contribution_status',
|
||||
length: 12,
|
||||
nullable: false,
|
||||
collation: 'utf8mb4_unicode_ci',
|
||||
})
|
||||
contributionStatus: string
|
||||
|
||||
@Column({ unsigned: true, nullable: true, name: 'transaction_id' })
|
||||
transactionId: number
|
||||
|
||||
@DeleteDateColumn({ name: 'deleted_at' })
|
||||
deletedAt: Date | null
|
||||
|
||||
@DeleteDateColumn({ unsigned: true, nullable: true, name: 'deleted_by' })
|
||||
deletedBy: number
|
||||
|
||||
@OneToMany(() => ContributionMessage, (message) => message.contribution)
|
||||
@JoinColumn({ name: 'contribution_id' })
|
||||
messages?: ContributionMessage[]
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export { Contribution } from './0047-messages_tables/Contribution'
|
||||
export { Contribution } from './0051-add_delete_by_to_contributions/Contribution'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { EventProtocol } from './0043-add_event_protocol_table/EventProtocol'
|
||||
export { EventProtocol } from './0050-add_messageId_to_event_protocol/EventProtocol'
|
||||
|
||||
@ -1 +1 @@
|
||||
export { User } from './0047-messages_tables/User'
|
||||
export { User } from './0049-add_user_contacts_table/User'
|
||||
|
||||
1
database/entity/UserContact.ts
Normal file
1
database/entity/UserContact.ts
Normal file
@ -0,0 +1 @@
|
||||
export { UserContact } from './0049-add_user_contacts_table/UserContact'
|
||||
@ -5,6 +5,7 @@ import { Migration } from './Migration'
|
||||
import { Transaction } from './Transaction'
|
||||
import { TransactionLink } from './TransactionLink'
|
||||
import { User } from './User'
|
||||
import { UserContact } from './UserContact'
|
||||
import { Contribution } from './Contribution'
|
||||
import { EventProtocol } from './EventProtocol'
|
||||
import { ContributionMessage } from './ContributionMessage'
|
||||
@ -20,4 +21,5 @@ export const entities = [
|
||||
User,
|
||||
EventProtocol,
|
||||
ContributionMessage,
|
||||
UserContact,
|
||||
]
|
||||
|
||||
97
database/migrations/0049-add_user_contacts_table.ts
Normal file
97
database/migrations/0049-add_user_contacts_table.ts
Normal file
@ -0,0 +1,97 @@
|
||||
/* MIGRATION TO ADD GRADIDO_ID
|
||||
*
|
||||
* This migration adds new columns to the table `users` and creates the
|
||||
* new table `user_contacts`
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE IF NOT EXISTS \`user_contacts\` (
|
||||
\`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
\`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
\`user_id\` int(10) unsigned NOT NULL,
|
||||
\`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL UNIQUE,
|
||||
\`email_verification_code\` bigint(20) unsigned DEFAULT NULL UNIQUE,
|
||||
\`email_opt_in_type_id\` int DEFAULT NULL,
|
||||
\`email_resend_count\` int DEFAULT '0',
|
||||
\`email_checked\` tinyint(4) NOT NULL DEFAULT 0,
|
||||
\`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
|
||||
\`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
\`updated_at\` datetime(3) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
\`deleted_at\` datetime(3) NULL DEFAULT NULL,
|
||||
PRIMARY KEY (\`id\`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
|
||||
|
||||
await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;')
|
||||
// define datetime column with a precision of 3 milliseconds
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` MODIFY COLUMN `created` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) AFTER `email_hash`;',
|
||||
)
|
||||
// define datetime column with a precision of 3 milliseconds
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` MODIFY COLUMN `deletedAt` datetime(3) NULL DEFAULT NULL AFTER `last_name`;',
|
||||
)
|
||||
// define datetime column with a precision of 3 milliseconds
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` MODIFY COLUMN `is_admin` datetime(3) NULL DEFAULT NULL AFTER `language`;',
|
||||
)
|
||||
|
||||
// merge values from login_email_opt_in table with users.email in new user_contacts table
|
||||
await queryFn(`
|
||||
INSERT INTO user_contacts
|
||||
(type, user_id, email, email_verification_code, email_opt_in_type_id, email_resend_count, email_checked, created_at, updated_at, deleted_at)
|
||||
SELECT 'EMAIL', users.id, users.email, optin.verification_code, optin.email_opt_in_type_id, optin.resend_count, users.email_checked, users.created, null, users.deletedAt
|
||||
FROM users LEFT JOIN
|
||||
(SELECT le.id, le.user_id, le.verification_code, le.email_opt_in_type_id, le.resend_count, le.created, le.updated,
|
||||
ROW_NUMBER() OVER (PARTITION BY le.user_id ORDER BY le.created DESC) AS row_num
|
||||
FROM login_email_opt_in as le) AS optin ON users.id = optin.user_id AND row_num = 1;`)
|
||||
|
||||
// insert in users table the email_id of the new created email-contacts
|
||||
const contacts = await queryFn(`SELECT c.id, c.user_id FROM user_contacts as c`)
|
||||
for (const id in contacts) {
|
||||
const contact = contacts[id]
|
||||
await queryFn(
|
||||
`UPDATE users as u SET u.email_id = "${contact.id}" WHERE u.id = "${contact.user_id}"`,
|
||||
)
|
||||
}
|
||||
// these steps comes after verification and test
|
||||
await queryFn('ALTER TABLE users DROP COLUMN email;')
|
||||
await queryFn('ALTER TABLE users DROP COLUMN email_checked;')
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// this step comes after verification and test
|
||||
await queryFn('ALTER TABLE users ADD COLUMN email varchar(255) NULL AFTER privkey;')
|
||||
await queryFn(
|
||||
'ALTER TABLE users ADD COLUMN email_checked tinyint(4) NOT NULL DEFAULT 0 AFTER email;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` MODIFY COLUMN `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `email_hash`;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` MODIFY COLUMN `deletedAt` datetime NULL DEFAULT NULL AFTER `last_name`;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `users` MODIFY COLUMN `is_admin` datetime NULL DEFAULT NULL AFTER `language`;',
|
||||
)
|
||||
|
||||
// reconstruct the previous email back from contacts to users table
|
||||
const contacts = await queryFn(
|
||||
`SELECT c.id, c.email, c.user_id, c.email_checked FROM user_contacts as c`,
|
||||
)
|
||||
for (const id in contacts) {
|
||||
const contact = contacts[id]
|
||||
await queryFn(
|
||||
`UPDATE users SET email = "${contact.email}", email_checked="${contact.email_checked}" WHERE id = "${contact.user_id}" and email_id = "${contact.id}"`,
|
||||
)
|
||||
}
|
||||
await queryFn('ALTER TABLE users MODIFY COLUMN email varchar(255) NOT NULL UNIQUE;')
|
||||
|
||||
// write downgrade logic as parameter of queryFn
|
||||
await queryFn(`DROP TABLE IF EXISTS user_contacts;`)
|
||||
|
||||
await queryFn('ALTER TABLE users DROP COLUMN email_id;')
|
||||
}
|
||||
12
database/migrations/0050-add_messageId_to_event_protocol.ts
Normal file
12
database/migrations/0050-add_messageId_to_event_protocol.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
`ALTER TABLE \`event_protocol\` ADD COLUMN \`message_id\` int(10) unsigned NULL DEFAULT NULL;`,
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`ALTER TABLE \`event_protocol\` DROP COLUMN \`message_id\`;`)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user