mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge remote-tracking branch 'origin/master' into
1574-Concept_to_introduce_Gradido-ID
This commit is contained in:
commit
f268651700
48
CHANGELOG.md
48
CHANGELOG.md
@ -4,8 +4,56 @@ 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).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [1.12.1](https://github.com/gradido/gradido/compare/1.12.0...1.12.1)
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
|
||||||
|
> 12 September 2022
|
||||||
|
|
||||||
|
- release: v1.12.0 [`#2191`](https://github.com/gradido/gradido/pull/2191)
|
||||||
|
- if message empty else disabled button [`#2189`](https://github.com/gradido/gradido/pull/2189)
|
||||||
|
- messages show if Confirmed [`#2185`](https://github.com/gradido/gradido/pull/2185)
|
||||||
|
- text in messages smaller [`#2186`](https://github.com/gradido/gradido/pull/2186)
|
||||||
|
- feat: 🍰 Klicktipp retrieve not registered email [`#2181`](https://github.com/gradido/gradido/pull/2181)
|
||||||
|
- fix: 🍰 isModerator on messages to switch the messages side in the messages overview [`#2182`](https://github.com/gradido/gradido/pull/2182)
|
||||||
|
- Refactor locales for Nederlands [`#2174`](https://github.com/gradido/gradido/pull/2174)
|
||||||
|
- Add is moderator to contribution message [`#2180`](https://github.com/gradido/gradido/pull/2180)
|
||||||
|
- feat: 🍰 Moderator Cannot Answer Himself [`#2178`](https://github.com/gradido/gradido/pull/2178)
|
||||||
|
- refactor: Improve Statistics Query [`#2170`](https://github.com/gradido/gradido/pull/2170)
|
||||||
|
- fix: Remove Statistics from Wallet [`#2171`](https://github.com/gradido/gradido/pull/2171)
|
||||||
|
- feat: 🍰 Contribution Messages In Frontend [`#2164`](https://github.com/gradido/gradido/pull/2164)
|
||||||
|
- feat: 🚀 CRUD For Contribution Messages [`#2149`](https://github.com/gradido/gradido/pull/2149)
|
||||||
|
- fix: 🍰 Decay Calculation In Community Statistics [`#2167`](https://github.com/gradido/gradido/pull/2167)
|
||||||
|
- chore: 🍰 Remove Fetch Policy Network Only From Statistics [`#2159`](https://github.com/gradido/gradido/pull/2159)
|
||||||
|
- feat: 🍰 Remove Some Statistics Data From Frontend [`#2153`](https://github.com/gradido/gradido/pull/2153)
|
||||||
|
- feat: 🍰 Add Toogle Collaps On Language Name [`#2156`](https://github.com/gradido/gradido/pull/2156)
|
||||||
|
- 2145 corrections style for frontend [`#2147`](https://github.com/gradido/gradido/pull/2147)
|
||||||
|
- 2072 feature usecase contribution messaging [`#2073`](https://github.com/gradido/gradido/pull/2073)
|
||||||
|
- 2151 add hint to redeem link [`#2158`](https://github.com/gradido/gradido/pull/2158)
|
||||||
|
- 🍰 Create `contribution messages` table [`#2137`](https://github.com/gradido/gradido/pull/2137)
|
||||||
|
- feat: 🍰 Add The Languages French And Dutch [`#2138`](https://github.com/gradido/gradido/pull/2138)
|
||||||
|
- 1973 list open contribution links in the wallet [`#1975`](https://github.com/gradido/gradido/pull/1975)
|
||||||
|
- feat: 🍰 Admin Interface Displays Statistics [`#2124`](https://github.com/gradido/gradido/pull/2124)
|
||||||
|
- feat: Statistics Resolver [`#2041`](https://github.com/gradido/gradido/pull/2041)
|
||||||
|
- 2116 retrieve admin and moderators [`#2127`](https://github.com/gradido/gradido/pull/2127)
|
||||||
|
- 2125 feature gradido id: new column gradidoid in users table [`#2126`](https://github.com/gradido/gradido/pull/2126)
|
||||||
|
- 2119 new menu item gdt [`#2120`](https://github.com/gradido/gradido/pull/2120)
|
||||||
|
- feat: Migrate Contributions Table [`#2136`](https://github.com/gradido/gradido/pull/2136)
|
||||||
|
- chore: 🍰 Refactor Contribution Form Logic And Write Tests [`#2092`](https://github.com/gradido/gradido/pull/2092)
|
||||||
|
- fix: 🍰 Add `emailChecked` Before Changing `optIn` State & Log Error On klicktipp Middleware [`#2107`](https://github.com/gradido/gradido/pull/2107)
|
||||||
|
- Add RIGHTS.LIST_CONTRIBUTION_LINKS to ROLE_USER [`#2123`](https://github.com/gradido/gradido/pull/2123)
|
||||||
|
- 2121 translate locales to spanish [`#2122`](https://github.com/gradido/gradido/pull/2122)
|
||||||
|
- add formatter on input amount replace point and comma [`#2115`](https://github.com/gradido/gradido/pull/2115)
|
||||||
|
- remove required from form.memo [`#2114`](https://github.com/gradido/gradido/pull/2114)
|
||||||
|
- Fix pagination ellipsis [`#2104`](https://github.com/gradido/gradido/pull/2104)
|
||||||
|
|
||||||
#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0)
|
#### [1.11.0](https://github.com/gradido/gradido/compare/1.10.1...1.11.0)
|
||||||
|
|
||||||
|
> 28 July 2022
|
||||||
|
|
||||||
|
- release: Version 1.11.0 [`#2103`](https://github.com/gradido/gradido/pull/2103)
|
||||||
- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102)
|
- Fix navbar community item [`#2102`](https://github.com/gradido/gradido/pull/2102)
|
||||||
- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101)
|
- Add validation date info to copied text after transaction link creation [`#2101`](https://github.com/gradido/gradido/pull/2101)
|
||||||
- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099)
|
- Remove member area from menu (desktop and mobile), when user has no elopage account [`#2099`](https://github.com/gradido/gradido/pull/2099)
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Administraion Interface for Gradido",
|
"description": "Administraion Interface for Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Moriz Wahl",
|
"author": "Moriz Wahl",
|
||||||
"version": "1.11.0",
|
"version": "1.12.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -15,7 +15,10 @@
|
|||||||
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
||||||
<b-card>
|
<b-card>
|
||||||
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
|
<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-card>
|
||||||
</b-collapse>
|
</b-collapse>
|
||||||
|
|
||||||
@ -24,6 +27,7 @@
|
|||||||
v-if="count > 0"
|
v-if="count > 0"
|
||||||
:items="items"
|
:items="items"
|
||||||
@editContributionLinkData="editContributionLinkData"
|
@editContributionLinkData="editContributionLinkData"
|
||||||
|
@get-contribution-links="$emit('get-contribution-links')"
|
||||||
/>
|
/>
|
||||||
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
||||||
</b-card-text>
|
</b-card-text>
|
||||||
|
|||||||
@ -163,7 +163,6 @@ export default {
|
|||||||
if (this.form.validFrom === null)
|
if (this.form.validFrom === null)
|
||||||
return this.toastError(this.$t('contributionLink.noStartDate'))
|
return this.toastError(this.$t('contributionLink.noStartDate'))
|
||||||
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
||||||
// alert(JSON.stringify(this.form))
|
|
||||||
this.$apollo
|
this.$apollo
|
||||||
.mutate({
|
.mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -182,6 +181,8 @@ export default {
|
|||||||
this.link = result.data.createContributionLink.link
|
this.link = result.data.createContributionLink.link
|
||||||
this.toastSuccess(this.link)
|
this.toastSuccess(this.link)
|
||||||
this.onReset()
|
this.onReset()
|
||||||
|
this.$root.$emit('bv::toggle::collapse', 'newContribution')
|
||||||
|
this.$emit('get-contribution-links')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toastError(error.message)
|
this.toastError(error.message)
|
||||||
|
|||||||
@ -95,7 +95,7 @@ describe('ContributionLinkList', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('toasts a success message', () => {
|
it('toasts a success message', () => {
|
||||||
expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
|
expect(toastSuccessSpy).toBeCalledWith('contributionLink.deleted')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contribution-link-list">
|
<div class="contribution-link-list">
|
||||||
<b-table striped hover :items="items" :fields="fields">
|
<b-table striped hover :items="items" :fields="fields">
|
||||||
<template #cell(delete)>
|
<template #cell(delete)="data">
|
||||||
<b-button
|
<b-button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
size="md"
|
size="md"
|
||||||
class="mr-2 test-delete-link"
|
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-icon icon="trash" variant="light"></b-icon>
|
||||||
</b-button>
|
</b-button>
|
||||||
@ -34,7 +34,7 @@
|
|||||||
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
||||||
</template>
|
</template>
|
||||||
<b-card-text>
|
<b-card-text>
|
||||||
{{ modalData }}
|
{{ modalData.memo ? modalData.memo : '' }}
|
||||||
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
||||||
</b-card-text>
|
</b-card-text>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@ -70,28 +70,30 @@ export default {
|
|||||||
'edit',
|
'edit',
|
||||||
'show',
|
'show',
|
||||||
],
|
],
|
||||||
modalData: null,
|
modalData: {},
|
||||||
modalDataLink: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
deleteContributionLink() {
|
deleteContributionLink(id, name) {
|
||||||
this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
|
this.$bvModal
|
||||||
if (value)
|
.msgBoxConfirm(this.$t('contributionLink.deleteNow', { name: name }))
|
||||||
await this.$apollo
|
.then(async (value) => {
|
||||||
.mutate({
|
if (value)
|
||||||
mutation: deleteContributionLink,
|
await this.$apollo
|
||||||
variables: {
|
.mutate({
|
||||||
id: this.id,
|
mutation: deleteContributionLink,
|
||||||
},
|
variables: {
|
||||||
})
|
id: id,
|
||||||
.then(() => {
|
},
|
||||||
this.toastSuccess('TODO: request message deleted ')
|
})
|
||||||
})
|
.then(() => {
|
||||||
.catch((err) => {
|
this.toastSuccess(this.$t('contributionLink.deleted'))
|
||||||
this.toastError(err.message)
|
this.$emit('get-contribution-links')
|
||||||
})
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
|
this.toastError(err.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
editContributionLink(row) {
|
editContributionLink(row) {
|
||||||
this.$emit('editContributionLinkData', row)
|
this.$emit('editContributionLinkData', row)
|
||||||
|
|||||||
@ -0,0 +1,111 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ContributionMessagesFormular from './ContributionMessagesFormular.vue'
|
||||||
|
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const apolloMutateMock = jest.fn().mockResolvedValue()
|
||||||
|
|
||||||
|
describe('ContributionMessagesFormular', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
contributionId: 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$apollo: {
|
||||||
|
mutate: apolloMutateMock,
|
||||||
|
},
|
||||||
|
$i18n: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(ContributionMessagesFormular, {
|
||||||
|
localVue,
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV .contribution-messages-formular', () => {
|
||||||
|
expect(wrapper.find('div.contribution-messages-formular').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('on trigger reset', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper.setData({
|
||||||
|
form: {
|
||||||
|
text: 'text form message',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await wrapper.find('form').trigger('reset')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('form has empty text', () => {
|
||||||
|
expect(wrapper.vm.form).toEqual({
|
||||||
|
text: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('on trigger submit', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper.setData({
|
||||||
|
form: {
|
||||||
|
text: 'text form message',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emitted "get-list-contribution-messages" with data', async () => {
|
||||||
|
expect(wrapper.emitted('get-list-contribution-messages')).toEqual(
|
||||||
|
expect.arrayContaining([expect.arrayContaining([42])]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emitted "update-state" with data', async () => {
|
||||||
|
expect(wrapper.emitted('update-state')).toEqual(
|
||||||
|
expect.arrayContaining([expect.arrayContaining([42])]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('send contribution message with error', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
apolloMutateMock.mockRejectedValue({ message: 'OUCH!' })
|
||||||
|
wrapper = Wrapper()
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts an error message', () => {
|
||||||
|
expect(toastErrorSpy).toBeCalledWith('OUCH!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('send contribution message with success', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapper.setData({
|
||||||
|
form: {
|
||||||
|
text: 'text form message',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
wrapper = Wrapper()
|
||||||
|
await wrapper.find('form').trigger('submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('toasts an success message', () => {
|
||||||
|
expect(toastSuccessSpy).toBeCalledWith('message.request')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contribution-messages-formular">
|
||||||
|
<div class="mt-5">
|
||||||
|
<b-form @submit.prevent="onSubmit" @reset.prevent="onReset">
|
||||||
|
<b-form-textarea
|
||||||
|
id="textarea"
|
||||||
|
v-model="form.text"
|
||||||
|
:placeholder="$t('contributionLink.memo')"
|
||||||
|
rows="3"
|
||||||
|
></b-form-textarea>
|
||||||
|
<b-row class="mt-4 mb-6">
|
||||||
|
<b-col>
|
||||||
|
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
|
||||||
|
</b-col>
|
||||||
|
<b-col class="text-right">
|
||||||
|
<b-button type="submit" variant="primary" :disabled="disabled">
|
||||||
|
{{ $t('form.submit') }}
|
||||||
|
</b-button>
|
||||||
|
</b-col>
|
||||||
|
</b-row>
|
||||||
|
</b-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { adminCreateContributionMessage } from '@/graphql/adminCreateContributionMessage'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContributionMessagesFormular',
|
||||||
|
props: {
|
||||||
|
contributionId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
text: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSubmit(event) {
|
||||||
|
this.$apollo
|
||||||
|
.mutate({
|
||||||
|
mutation: adminCreateContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: this.contributionId,
|
||||||
|
message: this.form.text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.$emit('get-list-contribution-messages', this.contributionId)
|
||||||
|
this.$emit('update-state', this.contributionId)
|
||||||
|
this.form.text = ''
|
||||||
|
this.toastSuccess(this.$t('message.request'))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toastError(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onReset(event) {
|
||||||
|
this.form.text = ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
disabled() {
|
||||||
|
if (this.form.text !== '') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ContributionMessagesList from './ContributionMessagesList.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
const apolloQueryMock = jest.fn().mockResolvedValue()
|
||||||
|
|
||||||
|
describe('ContributionMessagesList', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
contributionId: 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$i18n: {
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
$apollo: {
|
||||||
|
query: apolloQueryMock,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = () => {
|
||||||
|
return mount(ContributionMessagesList, {
|
||||||
|
localVue,
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends query to Apollo when created', () => {
|
||||||
|
expect(apolloQueryMock).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
variables: {
|
||||||
|
contributionId: propsData.contributionId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV .contribution-messages-list', () => {
|
||||||
|
expect(wrapper.find('div.contribution-messages-list').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a Component ContributionMessagesFormular', () => {
|
||||||
|
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="contribution-messages-list">
|
||||||
|
<b-container>
|
||||||
|
{{ messages.lenght }}
|
||||||
|
<div v-for="message in messages" v-bind:key="message.id">
|
||||||
|
<contribution-messages-list-item :message="message" />
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
|
||||||
|
<contribution-messages-formular
|
||||||
|
:contributionId="contributionId"
|
||||||
|
@get-list-contribution-messages="getListContributionMessages"
|
||||||
|
@update-state="updateState"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ContributionMessagesListItem from './slots/ContributionMessagesListItem.vue'
|
||||||
|
import ContributionMessagesFormular from '../ContributionMessages/ContributionMessagesFormular.vue'
|
||||||
|
import { listContributionMessages } from '../../graphql/listContributionMessages.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContributionMessagesList',
|
||||||
|
components: {
|
||||||
|
ContributionMessagesListItem,
|
||||||
|
ContributionMessagesFormular,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
contributionId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
messages: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getListContributionMessages(id) {
|
||||||
|
this.$apollo
|
||||||
|
.query({
|
||||||
|
query: listContributionMessages,
|
||||||
|
variables: {
|
||||||
|
contributionId: id,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
this.messages = result.data.listContributionMessages.messages
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.toastError(error.message)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateState(id) {
|
||||||
|
this.$emit('update-state', id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getListContributionMessages(this.contributionId)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.temp-message {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import ContributionMessagesListItem from './ContributionMessagesListItem.vue'
|
||||||
|
|
||||||
|
const localVue = global.localVue
|
||||||
|
|
||||||
|
describe('ContributionMessagesListItem', () => {
|
||||||
|
let wrapper
|
||||||
|
|
||||||
|
const mocks = {
|
||||||
|
$t: jest.fn((t) => t),
|
||||||
|
$d: jest.fn((d) => d),
|
||||||
|
$store: {
|
||||||
|
state: {
|
||||||
|
moderator: {
|
||||||
|
id: 107,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const propsData = {
|
||||||
|
contributionId: 42,
|
||||||
|
state: 'PENDING',
|
||||||
|
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(ContributionMessagesListItem, {
|
||||||
|
localVue,
|
||||||
|
mocks,
|
||||||
|
propsData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper = Wrapper()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a DIV .contribution-messages-list-item', () => {
|
||||||
|
expect(wrapper.find('div.contribution-messages-list-item').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('props.message.default', () => {
|
||||||
|
expect(wrapper.vm.$options.props.message.default.call()).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import IsModerator from '@/components/ContributionMessages/slots/IsModerator.vue'
|
||||||
|
import IsNotModerator from '@/components/ContributionMessages/slots/IsNotModerator.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContributionMessagesListItem',
|
||||||
|
components: {
|
||||||
|
IsModerator,
|
||||||
|
IsNotModerator,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
message: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
default() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
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({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
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({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
<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({
|
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
creationTransactionList: [
|
creationTransactionList: {
|
||||||
{
|
contributionCount: 2,
|
||||||
id: 1,
|
contributionList: [
|
||||||
amount: 100,
|
{
|
||||||
balanceDate: 0,
|
id: 1,
|
||||||
creationDate: new Date(),
|
amount: 5.8,
|
||||||
memo: 'Testing',
|
createdAt: '2022-09-21T11:09:51.000Z',
|
||||||
linkedUser: {
|
confirmedAt: null,
|
||||||
firstName: 'Gradido',
|
contributionDate: '2022-08-01T00:00:00.000Z',
|
||||||
lastName: 'Akademie',
|
memo: 'für deine Hilfe, Fräulein Rottenmeier',
|
||||||
|
state: 'PENDING',
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 2,
|
||||||
id: 2,
|
amount: '47',
|
||||||
amount: 200,
|
createdAt: '2022-09-21T11:09:28.000Z',
|
||||||
balanceDate: 0,
|
confirmedAt: '2022-09-21T11:09:28.000Z',
|
||||||
creationDate: new Date(),
|
contributionDate: '2022-08-01T00:00:00.000Z',
|
||||||
memo: 'Testing 2',
|
memo: 'für deine Hilfe, Frau Holle',
|
||||||
linkedUser: {
|
state: 'CONFIRMED',
|
||||||
firstName: 'Gradido',
|
|
||||||
lastName: 'Akademie',
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ const mocks = {
|
|||||||
|
|
||||||
const propsData = {
|
const propsData = {
|
||||||
userId: 1,
|
userId: 1,
|
||||||
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
fields: ['createdAt', 'contributionDate', 'confirmedAt', 'amount', 'memo'],
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('CreationTransactionList', () => {
|
describe('CreationTransactionList', () => {
|
||||||
@ -63,7 +62,7 @@ describe('CreationTransactionList', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
variables: {
|
variables: {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 10,
|
||||||
order: 'DESC',
|
order: 'DESC',
|
||||||
userId: 1,
|
userId: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="component-creation-transaction-list">
|
<div class="component-creation-transaction-list">
|
||||||
<div class="h3">{{ $t('transactionlist.title') }}</div>
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@ -13,14 +50,37 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
items: [],
|
||||||
|
rows: 0,
|
||||||
|
currentPage: 1,
|
||||||
|
perPage: 10,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'creationDate',
|
key: 'createdAt',
|
||||||
label: this.$t('transactionlist.date'),
|
label: this.$t('transactionlist.submitted'),
|
||||||
formatter: (value, key, item) => {
|
formatter: (value, key, item) => {
|
||||||
return this.$d(new Date(value))
|
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',
|
key: 'amount',
|
||||||
label: this.$t('transactionlist.amount'),
|
label: this.$t('transactionlist.amount'),
|
||||||
@ -28,23 +88,8 @@ export default {
|
|||||||
return `${value} GDD`
|
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: '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: {
|
methods: {
|
||||||
@ -53,14 +98,15 @@ export default {
|
|||||||
.query({
|
.query({
|
||||||
query: creationTransactionList,
|
query: creationTransactionList,
|
||||||
variables: {
|
variables: {
|
||||||
currentPage: 1,
|
currentPage: this.currentPage,
|
||||||
pageSize: 25,
|
pageSize: this.perPage,
|
||||||
order: 'DESC',
|
order: 'DESC',
|
||||||
userId: parseInt(this.userId),
|
userId: parseInt(this.userId),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.items = result.data.creationTransactionList
|
this.rows = result.data.creationTransactionList.contributionCount
|
||||||
|
this.items = result.data.creationTransactionList.contributionList
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.toastError(error.message)
|
this.toastError(error.message)
|
||||||
@ -70,5 +116,10 @@ export default {
|
|||||||
created() {
|
created() {
|
||||||
this.getTransactions()
|
this.getTransactions()
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
currentPage() {
|
||||||
|
this.getTransactions()
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -12,20 +12,42 @@
|
|||||||
</b-button>
|
</b-button>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(editCreation)="row">
|
<template #cell(editCreation)="row">
|
||||||
<b-button
|
<div v-if="$store.state.moderator.id !== row.item.userId">
|
||||||
v-if="row.item.moderator"
|
<b-button
|
||||||
variant="info"
|
v-if="row.item.moderator"
|
||||||
size="md"
|
variant="info"
|
||||||
@click="rowToggleDetails(row, 0)"
|
size="md"
|
||||||
class="mr-2"
|
@click="rowToggleDetails(row, 0)"
|
||||||
>
|
class="mr-2"
|
||||||
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
>
|
||||||
</b-button>
|
<b-icon :icon="row.detailsShowing ? 'x' : 'pencil-square'" aria-label="Help"></b-icon>
|
||||||
|
</b-button>
|
||||||
|
<b-button v-else @click="rowToggleDetails(row, 0)">
|
||||||
|
<b-icon icon="chat-dots"></b-icon>
|
||||||
|
<b-icon
|
||||||
|
v-if="row.item.state === 'PENDING' && row.item.messageCount > 0"
|
||||||
|
icon="exclamation-circle-fill"
|
||||||
|
variant="warning"
|
||||||
|
></b-icon>
|
||||||
|
<b-icon
|
||||||
|
v-if="row.item.state === 'IN_PROGRESS' && row.item.messageCount > 0"
|
||||||
|
icon="question-diamond"
|
||||||
|
variant="light"
|
||||||
|
></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #cell(confirm)="row">
|
<template #cell(confirm)="row">
|
||||||
<b-button variant="success" size="md" @click="$emit('show-overlay', row.item)" class="mr-2">
|
<div v-if="$store.state.moderator.id !== row.item.userId">
|
||||||
<b-icon icon="check" scale="2" variant=""></b-icon>
|
<b-button
|
||||||
</b-button>
|
variant="success"
|
||||||
|
size="md"
|
||||||
|
@click="$emit('show-overlay', row.item)"
|
||||||
|
class="mr-2"
|
||||||
|
>
|
||||||
|
<b-icon icon="check" scale="2" variant=""></b-icon>
|
||||||
|
</b-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #row-details="row">
|
<template #row-details="row">
|
||||||
<row-details
|
<row-details
|
||||||
@ -33,10 +55,10 @@
|
|||||||
type="show-creation"
|
type="show-creation"
|
||||||
slotName="show-creation"
|
slotName="show-creation"
|
||||||
:index="0"
|
:index="0"
|
||||||
@row-toggle-details="rowToggleDetails"
|
@row-toggle-details="rowToggleDetails(row, 0)"
|
||||||
>
|
>
|
||||||
<template #show-creation>
|
<template #show-creation>
|
||||||
<div>
|
<div v-if="row.item.moderator">
|
||||||
<edit-creation-formular
|
<edit-creation-formular
|
||||||
type="singleCreation"
|
type="singleCreation"
|
||||||
:creation="row.item.creation"
|
:creation="row.item.creation"
|
||||||
@ -44,6 +66,12 @@
|
|||||||
:row="row"
|
:row="row"
|
||||||
:creationUserData="creationUserData"
|
:creationUserData="creationUserData"
|
||||||
@update-creation-data="updateCreationData"
|
@update-creation-data="updateCreationData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<contribution-messages-list
|
||||||
|
:contributionId="row.item.id"
|
||||||
|
@update-state="updateState"
|
||||||
@update-user-data="updateUserData"
|
@update-user-data="updateUserData"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -58,6 +86,7 @@
|
|||||||
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
|
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
|
||||||
import RowDetails from '../RowDetails.vue'
|
import RowDetails from '../RowDetails.vue'
|
||||||
import EditCreationFormular from '../EditCreationFormular.vue'
|
import EditCreationFormular from '../EditCreationFormular.vue'
|
||||||
|
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'OpenCreationsTable',
|
name: 'OpenCreationsTable',
|
||||||
@ -65,6 +94,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
EditCreationFormular,
|
EditCreationFormular,
|
||||||
RowDetails,
|
RowDetails,
|
||||||
|
ContributionMessagesList,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
@ -98,6 +128,9 @@ export default {
|
|||||||
updateUserData(rowItem, newCreation) {
|
updateUserData(rowItem, newCreation) {
|
||||||
rowItem.creation = newCreation
|
rowItem.creation = newCreation
|
||||||
},
|
},
|
||||||
|
updateState(id) {
|
||||||
|
this.$emit('update-state', id)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
15
admin/src/graphql/adminCreateContributionMessage.js
Normal file
15
admin/src/graphql/adminCreateContributionMessage.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const adminCreateContributionMessage = gql`
|
||||||
|
mutation ($contributionId: Float!, $message: String!) {
|
||||||
|
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
type
|
||||||
|
userFirstName
|
||||||
|
userLastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -8,14 +8,15 @@ export const creationTransactionList = gql`
|
|||||||
order: $order
|
order: $order
|
||||||
userId: $userId
|
userId: $userId
|
||||||
) {
|
) {
|
||||||
id
|
contributionCount
|
||||||
amount
|
contributionList {
|
||||||
balanceDate
|
id
|
||||||
creationDate
|
amount
|
||||||
memo
|
createdAt
|
||||||
linkedUser {
|
confirmedAt
|
||||||
firstName
|
contributionDate
|
||||||
lastName
|
memo
|
||||||
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
admin/src/graphql/listContributionMessages.js
Normal file
25
admin/src/graphql/listContributionMessages.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const listContributionMessages = gql`
|
||||||
|
query ($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
|
||||||
|
listContributionMessages(
|
||||||
|
contributionId: $contributionId
|
||||||
|
pageSize: $pageSize
|
||||||
|
currentPage: $currentPage
|
||||||
|
order: $order
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
messages {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
type
|
||||||
|
userFirstName
|
||||||
|
userLastName
|
||||||
|
userId
|
||||||
|
isModerator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@ -6,12 +6,15 @@ export const listUnconfirmedContributions = gql`
|
|||||||
id
|
id
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
|
userId
|
||||||
email
|
email
|
||||||
amount
|
amount
|
||||||
memo
|
memo
|
||||||
date
|
date
|
||||||
moderator
|
moderator
|
||||||
creation
|
creation
|
||||||
|
state
|
||||||
|
messageCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -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,7 +7,8 @@
|
|||||||
"contributionLinks": "Beitragslinks",
|
"contributionLinks": "Beitragslinks",
|
||||||
"create": "Anlegen",
|
"create": "Anlegen",
|
||||||
"cycle": "Zyklus",
|
"cycle": "Zyklus",
|
||||||
"deleteNow": "Automatische Creations wirklich löschen?",
|
"deleted": "Automatische Schöpfung gelöscht!",
|
||||||
|
"deleteNow": "Automatische Creations '{name}' wirklich löschen?",
|
||||||
"maximumAmount": "maximaler Betrag",
|
"maximumAmount": "maximaler Betrag",
|
||||||
"maxPerCycle": "Wiederholungen",
|
"maxPerCycle": "Wiederholungen",
|
||||||
"memo": "Nachricht",
|
"memo": "Nachricht",
|
||||||
@ -69,15 +70,32 @@
|
|||||||
},
|
},
|
||||||
"short_hash": "({shortHash})"
|
"short_hash": "({shortHash})"
|
||||||
},
|
},
|
||||||
|
"form": {
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"submit": "Senden"
|
||||||
|
},
|
||||||
"GDD": "GDD",
|
"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",
|
"hide_details": "Details verbergen",
|
||||||
"lastname": "Nachname",
|
"lastname": "Nachname",
|
||||||
"math": {
|
"math": {
|
||||||
"colon": ":",
|
"colon": ":",
|
||||||
|
"equals": "=",
|
||||||
"exclaim": "!",
|
"exclaim": "!",
|
||||||
"pipe": "|",
|
"pipe": "|",
|
||||||
"plus": "+"
|
"plus": "+"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"request": "Die Anfrage wurde gesendet."
|
||||||
|
},
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@ -126,10 +144,11 @@
|
|||||||
},
|
},
|
||||||
"transactionlist": {
|
"transactionlist": {
|
||||||
"amount": "Betrag",
|
"amount": "Betrag",
|
||||||
"balanceDate": "Schöpfungsdatum",
|
"confirmed": "Bestätigt",
|
||||||
"community": "Gemeinschaft",
|
|
||||||
"date": "Datum",
|
|
||||||
"memo": "Nachricht",
|
"memo": "Nachricht",
|
||||||
|
"period": "Zeitraum",
|
||||||
|
"state": "Status",
|
||||||
|
"submitted": "Eingereicht",
|
||||||
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
||||||
},
|
},
|
||||||
"undelete_user": "Nutzer wiederherstellen",
|
"undelete_user": "Nutzer wiederherstellen",
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
"contributionLinks": "Contribution Links",
|
"contributionLinks": "Contribution Links",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"cycle": "Cycle",
|
"cycle": "Cycle",
|
||||||
"deleteNow": "Do you really delete automatic creations?",
|
"deleted": "Automatic creation deleted!",
|
||||||
|
"deleteNow": "Do you really delete automatic creations '{name}'?",
|
||||||
"maximumAmount": "Maximum amount",
|
"maximumAmount": "Maximum amount",
|
||||||
"maxPerCycle": "Repetition",
|
"maxPerCycle": "Repetition",
|
||||||
"memo": "Memo",
|
"memo": "Memo",
|
||||||
@ -69,15 +70,32 @@
|
|||||||
},
|
},
|
||||||
"short_hash": "({shortHash})"
|
"short_hash": "({shortHash})"
|
||||||
},
|
},
|
||||||
|
"form": {
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"submit": "Send"
|
||||||
|
},
|
||||||
"GDD": "GDD",
|
"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",
|
"hide_details": "Hide details",
|
||||||
"lastname": "Lastname",
|
"lastname": "Lastname",
|
||||||
"math": {
|
"math": {
|
||||||
"colon": ":",
|
"colon": ":",
|
||||||
|
"equals": "=",
|
||||||
"exclaim": "!",
|
"exclaim": "!",
|
||||||
"pipe": "|",
|
"pipe": "|",
|
||||||
"plus": "+"
|
"plus": "+"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"request": "Request has been sent."
|
||||||
|
},
|
||||||
"moderator": "Moderator",
|
"moderator": "Moderator",
|
||||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@ -126,10 +144,11 @@
|
|||||||
},
|
},
|
||||||
"transactionlist": {
|
"transactionlist": {
|
||||||
"amount": "Amount",
|
"amount": "Amount",
|
||||||
"balanceDate": "Creation date",
|
"confirmed": "Confirmed",
|
||||||
"community": "Community",
|
|
||||||
"date": "Date",
|
|
||||||
"memo": "Message",
|
"memo": "Message",
|
||||||
|
"period": "Period",
|
||||||
|
"state": "State",
|
||||||
|
"submitted": "Submitted",
|
||||||
"title": "All creation-transactions for the user"
|
"title": "All creation-transactions for the user"
|
||||||
},
|
},
|
||||||
"undelete_user": "Undelete User",
|
"undelete_user": "Undelete User",
|
||||||
|
|||||||
@ -14,21 +14,23 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
|||||||
id: 1,
|
id: 1,
|
||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
|
userId: 99,
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
amount: 500,
|
amount: 500,
|
||||||
memo: 'Danke für alles',
|
memo: 'Danke für alles',
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
moderator: 2,
|
moderator: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
firstName: 'Räuber',
|
firstName: 'Räuber',
|
||||||
lastName: 'Hotzenplotz',
|
lastName: 'Hotzenplotz',
|
||||||
|
userId: 100,
|
||||||
email: 'raeuber@hotzenplotz.de',
|
email: 'raeuber@hotzenplotz.de',
|
||||||
amount: 1000000,
|
amount: 1000000,
|
||||||
memo: 'Gut Ergattert',
|
memo: 'Gut Ergattert',
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
moderator: 2,
|
moderator: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -41,6 +43,15 @@ const mocks = {
|
|||||||
$d: jest.fn((d) => d),
|
$d: jest.fn((d) => d),
|
||||||
$store: {
|
$store: {
|
||||||
commit: storeCommitMock,
|
commit: storeCommitMock,
|
||||||
|
state: {
|
||||||
|
moderator: {
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
isAdmin: '2022-08-30T07:41:31.000Z',
|
||||||
|
id: 263,
|
||||||
|
language: 'de',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
$apollo: {
|
$apollo: {
|
||||||
query: apolloQueryMock,
|
query: apolloQueryMock,
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
:fields="fields"
|
:fields="fields"
|
||||||
@remove-creation="removeCreation"
|
@remove-creation="removeCreation"
|
||||||
@show-overlay="showOverlay"
|
@show-overlay="showOverlay"
|
||||||
|
@update-state="updateState"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -93,6 +94,10 @@ export default {
|
|||||||
this.overlay = true
|
this.overlay = true
|
||||||
this.item = item
|
this.item = item
|
||||||
},
|
},
|
||||||
|
updateState(id) {
|
||||||
|
this.pendingCreations.find((obj) => obj.id === id).messagesCount++
|
||||||
|
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS'
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fields() {
|
fields() {
|
||||||
|
|||||||
@ -28,7 +28,11 @@
|
|||||||
</b-link>
|
</b-link>
|
||||||
</b-card-text>
|
</b-card-text>
|
||||||
</b-card>
|
</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" />
|
<community-statistic class="mt-5" v-model="statistics" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -89,7 +93,6 @@ export default {
|
|||||||
this.$apollo
|
this.$apollo
|
||||||
.query({
|
.query({
|
||||||
query: communityStatistics,
|
query: communityStatistics,
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
this.statistics.totalUsers = result.data.communityStatistics.totalUsers
|
this.statistics.totalUsers = result.data.communityStatistics.totalUsers
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const authLink = new ApolloLink((operation, forward) => {
|
|||||||
operation.setContext({
|
operation.setContext({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||||
|
clientRequestTime: new Date().toString(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return forward(operation).map((response) => {
|
return forward(operation).map((response) => {
|
||||||
|
|||||||
@ -94,6 +94,7 @@ describe('apolloProvider', () => {
|
|||||||
expect(setContextMock).toBeCalledWith({
|
expect(setContextMock).toBeCalledWith({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: 'Bearer some-token',
|
Authorization: 'Bearer some-token',
|
||||||
|
clientRequestTime: expect.any(String),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -109,6 +110,7 @@ describe('apolloProvider', () => {
|
|||||||
expect(setContextMock).toBeCalledWith({
|
expect(setContextMock).toBeCalledWith({
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: '',
|
Authorization: '',
|
||||||
|
clientRequestTime: expect.any(String),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
CONFIG_VERSION=v9.2022-07-07
|
CONFIG_VERSION=v10.2022-09-20
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=4000
|
||||||
@ -37,6 +37,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
|||||||
|
|
||||||
# EMail
|
# EMail
|
||||||
EMAIL=false
|
EMAIL=false
|
||||||
|
EMAIL_TEST_MODUS=false
|
||||||
|
EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||||
EMAIL_USERNAME=gradido_email
|
EMAIL_USERNAME=gradido_email
|
||||||
EMAIL_SENDER=info@gradido.net
|
EMAIL_SENDER=info@gradido.net
|
||||||
EMAIL_PASSWORD=xxx
|
EMAIL_PASSWORD=xxx
|
||||||
|
|||||||
@ -36,6 +36,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
|||||||
|
|
||||||
# EMail
|
# EMail
|
||||||
EMAIL=$EMAIL
|
EMAIL=$EMAIL
|
||||||
|
EMAIL_TEST_MODUS=$EMAIL_TEST_MODUS
|
||||||
|
EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER
|
||||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||||
EMAIL_SENDER=$EMAIL_SENDER
|
EMAIL_SENDER=$EMAIL_SENDER
|
||||||
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-backend",
|
"name": "gradido-backend",
|
||||||
"version": "1.11.0",
|
"version": "1.12.1",
|
||||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/backend",
|
"repository": "https://github.com/gradido/gradido/backend",
|
||||||
@ -14,7 +14,8 @@
|
|||||||
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
|
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
|
||||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||||
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
|
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
|
||||||
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts"
|
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
|
||||||
|
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^27.0.2",
|
||||||
|
|||||||
@ -33,6 +33,8 @@ export enum RIGHTS {
|
|||||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||||
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
|
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
|
||||||
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
||||||
|
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
|
||||||
|
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||||
// Admin
|
// Admin
|
||||||
SEARCH_USERS = 'SEARCH_USERS',
|
SEARCH_USERS = 'SEARCH_USERS',
|
||||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||||
@ -50,4 +52,5 @@ export enum RIGHTS {
|
|||||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||||
|
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,8 @@ export const ROLE_USER = new Role('user', [
|
|||||||
RIGHTS.SEARCH_ADMIN_USERS,
|
RIGHTS.SEARCH_ADMIN_USERS,
|
||||||
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
RIGHTS.LIST_CONTRIBUTION_LINKS,
|
||||||
RIGHTS.COMMUNITY_STATISTICS,
|
RIGHTS.COMMUNITY_STATISTICS,
|
||||||
|
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
|
||||||
|
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||||
])
|
])
|
||||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||||
|
|
||||||
|
|||||||
@ -10,14 +10,14 @@ Decimal.set({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const constants = {
|
const constants = {
|
||||||
DB_VERSION: '0047-messages_tables',
|
DB_VERSION: '0049-add_user_contacts_table',
|
||||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||||
LOG4JS_CONFIG: 'log4js-config.json',
|
LOG4JS_CONFIG: 'log4js-config.json',
|
||||||
// default log level on production should be info
|
// default log level on production should be info
|
||||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||||
CONFIG_VERSION: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v9.2022-07-07',
|
EXPECTED: 'v10.2022-09-20',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -67,6 +67,8 @@ const loginServer = {
|
|||||||
|
|
||||||
const email = {
|
const email = {
|
||||||
EMAIL: process.env.EMAIL === 'true' || false,
|
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_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export class EventRegister extends EventBasicUserId {}
|
|||||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||||
export class EventInactiveAccount extends EventBasicUserId {}
|
export class EventInactiveAccount extends EventBasicUserId {}
|
||||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||||
|
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
|
||||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||||
export class EventLogin extends EventBasicUserId {}
|
export class EventLogin extends EventBasicUserId {}
|
||||||
@ -113,6 +114,15 @@ export class Event {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setEventSendAccountMultiRegistrationEmail(
|
||||||
|
ev: EventSendAccountMultiRegistrationEmail,
|
||||||
|
): Event {
|
||||||
|
this.setByBasicUser(ev.userId)
|
||||||
|
this.type = EventProtocolType.SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
|
||||||
this.setByBasicUser(ev.userId)
|
this.setByBasicUser(ev.userId)
|
||||||
this.type = EventProtocolType.CONFIRM_EMAIL
|
this.type = EventProtocolType.CONFIRM_EMAIL
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export enum EventProtocolType {
|
|||||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||||
|
SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL',
|
||||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||||
LOGIN = 'LOGIN',
|
LOGIN = 'LOGIN',
|
||||||
|
|||||||
11
backend/src/graphql/arg/ContributionMessageArgs.ts
Normal file
11
backend/src/graphql/arg/ContributionMessageArgs.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { ArgsType, Field, InputType } from 'type-graphql'
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
@ArgsType()
|
||||||
|
export default class ContributionMessageArgs {
|
||||||
|
@Field(() => Number)
|
||||||
|
contributionId: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
message: string
|
||||||
|
}
|
||||||
@ -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 - 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
|
// 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 {
|
try {
|
||||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||||
context.user = user
|
context.user = user
|
||||||
|
|||||||
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
|
||||||
|
})
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import { ObjectType, Field, Int } from 'type-graphql'
|
import { ObjectType, Field, Int } from 'type-graphql'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||||
import { User } from './User'
|
import { User } from '@entity/User'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Contribution {
|
export class Contribution {
|
||||||
constructor(contribution: dbContribution, user: User) {
|
constructor(contribution: dbContribution, user?: User | null) {
|
||||||
this.id = contribution.id
|
this.id = contribution.id
|
||||||
this.firstName = user ? user.firstName : null
|
this.firstName = user ? user.firstName : null
|
||||||
this.lastName = user ? user.lastName : null
|
this.lastName = user ? user.lastName : null
|
||||||
@ -16,6 +16,8 @@ export class Contribution {
|
|||||||
this.confirmedAt = contribution.confirmedAt
|
this.confirmedAt = contribution.confirmedAt
|
||||||
this.confirmedBy = contribution.confirmedBy
|
this.confirmedBy = contribution.confirmedBy
|
||||||
this.contributionDate = contribution.contributionDate
|
this.contributionDate = contribution.contributionDate
|
||||||
|
this.state = contribution.contributionStatus
|
||||||
|
this.messagesCount = contribution.messages ? contribution.messages.length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
@ -47,6 +49,12 @@ export class Contribution {
|
|||||||
|
|
||||||
@Field(() => Date)
|
@Field(() => Date)
|
||||||
contributionDate: Date
|
contributionDate: Date
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
messagesCount: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
state: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
53
backend/src/graphql/model/ContributionMessage.ts
Normal file
53
backend/src/graphql/model/ContributionMessage.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Field, ObjectType } from 'type-graphql'
|
||||||
|
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||||
|
import { User } from '@entity/User'
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ContributionMessage {
|
||||||
|
constructor(contributionMessage: DbContributionMessage, user: User) {
|
||||||
|
this.id = contributionMessage.id
|
||||||
|
this.message = contributionMessage.message
|
||||||
|
this.createdAt = contributionMessage.createdAt
|
||||||
|
this.updatedAt = contributionMessage.updatedAt
|
||||||
|
this.type = contributionMessage.type
|
||||||
|
this.userFirstName = user.firstName
|
||||||
|
this.userLastName = user.lastName
|
||||||
|
this.userId = user.id
|
||||||
|
this.isModerator = contributionMessage.isModerator
|
||||||
|
}
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
message: string
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
updatedAt?: Date | null
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
type: string
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
userFirstName: string | null
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
userLastName: string | null
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
userId: number | null
|
||||||
|
|
||||||
|
@Field(() => Boolean)
|
||||||
|
isModerator: boolean
|
||||||
|
}
|
||||||
|
@ObjectType()
|
||||||
|
export class ContributionMessageListResult {
|
||||||
|
@Field(() => Number)
|
||||||
|
count: number
|
||||||
|
|
||||||
|
@Field(() => [ContributionMessage])
|
||||||
|
messages: ContributionMessage[]
|
||||||
|
}
|
||||||
@ -5,7 +5,7 @@ import { User } from '@entity/User'
|
|||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UnconfirmedContribution {
|
export class UnconfirmedContribution {
|
||||||
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
|
constructor(contribution: Contribution, user: User | undefined, creations: Decimal[]) {
|
||||||
this.id = contribution.id
|
this.id = contribution.id
|
||||||
this.userId = contribution.userId
|
this.userId = contribution.userId
|
||||||
this.amount = contribution.amount
|
this.amount = contribution.amount
|
||||||
@ -13,8 +13,11 @@ export class UnconfirmedContribution {
|
|||||||
this.date = contribution.contributionDate
|
this.date = contribution.contributionDate
|
||||||
this.firstName = user ? user.firstName : ''
|
this.firstName = user ? user.firstName : ''
|
||||||
this.lastName = user ? user.lastName : ''
|
this.lastName = user ? user.lastName : ''
|
||||||
this.email = user ? user.email : ''
|
this.email = user ? user.emailContact.email : ''
|
||||||
|
this.moderator = contribution.moderatorId
|
||||||
this.creation = creations
|
this.creation = creations
|
||||||
|
this.state = contribution.contributionStatus
|
||||||
|
this.messageCount = contribution.messages ? contribution.messages.length : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@ -46,4 +49,10 @@ export class UnconfirmedContribution {
|
|||||||
|
|
||||||
@Field(() => [Decimal])
|
@Field(() => [Decimal])
|
||||||
creation: Decimal[]
|
creation: Decimal[]
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
|
state: string
|
||||||
|
|
||||||
|
@Field(() => Number)
|
||||||
|
messageCount: number
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { KlickTipp } from './KlickTipp'
|
|||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||||
|
import { UserContact } from './UserContact'
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class User {
|
export class User {
|
||||||
@ -10,12 +11,16 @@ export class User {
|
|||||||
this.id = user.id
|
this.id = user.id
|
||||||
this.gradidoID = user.gradidoID
|
this.gradidoID = user.gradidoID
|
||||||
this.alias = user.alias
|
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.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = user.deletedAt
|
||||||
this.createdAt = user.createdAt
|
this.createdAt = user.createdAt
|
||||||
this.emailChecked = user.emailChecked
|
|
||||||
this.language = user.language
|
this.language = user.language
|
||||||
this.publisherId = user.publisherId
|
this.publisherId = user.publisherId
|
||||||
this.isAdmin = user.isAdmin
|
this.isAdmin = user.isAdmin
|
||||||
@ -34,12 +39,18 @@ export class User {
|
|||||||
gradidoID: string
|
gradidoID: string
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
alias: string
|
alias?: string
|
||||||
|
|
||||||
|
@Field(() => Number, { nullable: true })
|
||||||
|
emailId: number | null
|
||||||
|
|
||||||
// TODO privacy issue here
|
// TODO privacy issue here
|
||||||
@Field(() => String)
|
@Field(() => String, { nullable: true })
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
@Field(() => UserContact)
|
||||||
|
emailContact: UserContact
|
||||||
|
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
firstName: string | null
|
firstName: string | null
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,11 @@ import { User } from '@entity/User'
|
|||||||
export class UserAdmin {
|
export class UserAdmin {
|
||||||
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||||
this.userId = user.id
|
this.userId = user.id
|
||||||
this.email = user.email
|
this.email = user.emailContact.email
|
||||||
this.firstName = user.firstName
|
this.firstName = user.firstName
|
||||||
this.lastName = user.lastName
|
this.lastName = user.lastName
|
||||||
this.creation = creation
|
this.creation = creation
|
||||||
this.emailChecked = user.emailChecked
|
this.emailChecked = user.emailContact.emailChecked
|
||||||
this.hasElopage = hasElopage
|
this.hasElopage = hasElopage
|
||||||
this.deletedAt = user.deletedAt
|
this.deletedAt = user.deletedAt
|
||||||
this.emailConfirmationSend = emailConfirmationSend
|
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
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ import Decimal from 'decimal.js-light'
|
|||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution } from '@entity/Contribution'
|
||||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||||
|
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||||
|
|
||||||
// mock account activation email to avoid console spam
|
// mock account activation email to avoid console spam
|
||||||
jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
||||||
@ -49,6 +50,14 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// mock account activation email to avoid console spam
|
||||||
|
jest.mock('@/mailer/sendContributionConfirmedEmail', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendContributionConfirmedEmail: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
let mutate: any, query: any, con: any
|
let mutate: any, query: any, con: any
|
||||||
let testEnv: any
|
let testEnv: any
|
||||||
|
|
||||||
@ -1117,7 +1126,9 @@ describe('AdminResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
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'),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -1450,6 +1461,20 @@ describe('AdminResolver', () => {
|
|||||||
expect(transaction[0].linkedUserId).toEqual(null)
|
expect(transaction[0].linkedUserId).toEqual(null)
|
||||||
expect(transaction[0].typeId).toEqual(1)
|
expect(transaction[0].typeId).toEqual(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('calls sendContributionConfirmedEmail', async () => {
|
||||||
|
expect(sendContributionConfirmedEmail).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||||
|
overviewURL: 'http://localhost/overview',
|
||||||
|
recipientEmail: 'bibi@bloxberg.de',
|
||||||
|
recipientFirstName: 'Bibi',
|
||||||
|
recipientLastName: 'Bloxberg',
|
||||||
|
senderFirstName: 'Peter',
|
||||||
|
senderLastName: 'Lustig',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('confirm two creations one after the other quickly', () => {
|
describe('confirm two creations one after the other quickly', () => {
|
||||||
@ -1493,6 +1518,7 @@ describe('AdminResolver', () => {
|
|||||||
)
|
)
|
||||||
await expect(r2).resolves.toEqual(
|
await expect(r2).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
|
// data: { confirmContribution: true },
|
||||||
errors: [new GraphQLError('Creation was not successful.')],
|
errors: [new GraphQLError('Creation was not successful.')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type
|
|||||||
import {
|
import {
|
||||||
getCustomRepository,
|
getCustomRepository,
|
||||||
IsNull,
|
IsNull,
|
||||||
Not,
|
|
||||||
ObjectLiteral,
|
|
||||||
getConnection,
|
getConnection,
|
||||||
In,
|
In,
|
||||||
MoreThan,
|
MoreThan,
|
||||||
@ -17,6 +15,7 @@ import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
|||||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||||
import { ContributionLink } from '@model/ContributionLink'
|
import { ContributionLink } from '@model/ContributionLink'
|
||||||
import { ContributionLinkList } from '@model/ContributionLinkList'
|
import { ContributionLinkList } from '@model/ContributionLinkList'
|
||||||
|
import { Contribution } from '@model/Contribution'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { UserRepository } from '@repository/User'
|
import { UserRepository } from '@repository/User'
|
||||||
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
||||||
@ -25,14 +24,11 @@ import SearchUsersArgs from '@arg/SearchUsersArgs'
|
|||||||
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
||||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||||
import { Transaction } from '@model/Transaction'
|
|
||||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { TransactionRepository } from '@repository/Transaction'
|
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||||
@ -43,8 +39,7 @@ import { Decay } from '@model/Decay'
|
|||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import { communityUser } from '@/util/communityUser'
|
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
@ -62,6 +57,14 @@ import {
|
|||||||
MEMO_MAX_CHARS,
|
MEMO_MAX_CHARS,
|
||||||
MEMO_MIN_CHARS,
|
MEMO_MIN_CHARS,
|
||||||
} from './const/const'
|
} 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_IN_REGISTER = 1
|
||||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||||
@ -75,24 +78,12 @@ export class AdminResolver {
|
|||||||
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||||
): Promise<SearchUsersResult> {
|
): Promise<SearchUsersResult> {
|
||||||
const userRepository = getCustomRepository(UserRepository)
|
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 = [
|
const userFields = [
|
||||||
'id',
|
'id',
|
||||||
'firstName',
|
'firstName',
|
||||||
'lastName',
|
'lastName',
|
||||||
'email',
|
'emailId',
|
||||||
'emailChecked',
|
'emailContact',
|
||||||
'deletedAt',
|
'deletedAt',
|
||||||
'isAdmin',
|
'isAdmin',
|
||||||
]
|
]
|
||||||
@ -101,7 +92,7 @@ export class AdminResolver {
|
|||||||
return 'user.' + fieldName
|
return 'user.' + fieldName
|
||||||
}),
|
}),
|
||||||
searchText,
|
searchText,
|
||||||
filterCriteria,
|
filters,
|
||||||
currentPage,
|
currentPage,
|
||||||
pageSize,
|
pageSize,
|
||||||
)
|
)
|
||||||
@ -118,32 +109,18 @@ export class AdminResolver {
|
|||||||
const adminUsers = await Promise.all(
|
const adminUsers = await Promise.all(
|
||||||
users.map(async (user) => {
|
users.map(async (user) => {
|
||||||
let emailConfirmationSend = ''
|
let emailConfirmationSend = ''
|
||||||
if (!user.emailChecked) {
|
if (!user.emailContact.emailChecked) {
|
||||||
const emailOptIn = await LoginEmailOptIn.findOne(
|
if (user.emailContact.updatedAt) {
|
||||||
{
|
emailConfirmationSend = user.emailContact.updatedAt.toISOString()
|
||||||
userId: user.id,
|
} else {
|
||||||
},
|
emailConfirmationSend = user.emailContact.createdAt.toISOString()
|
||||||
{
|
|
||||||
order: {
|
|
||||||
updatedAt: 'DESC',
|
|
||||||
createdAt: 'DESC',
|
|
||||||
},
|
|
||||||
select: ['updatedAt', 'createdAt'],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (emailOptIn) {
|
|
||||||
if (emailOptIn.updatedAt) {
|
|
||||||
emailConfirmationSend = emailOptIn.updatedAt.toISOString()
|
|
||||||
} else {
|
|
||||||
emailConfirmationSend = emailOptIn.createdAt.toISOString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const userCreations = creations.find((c) => c.id === user.id)
|
const userCreations = creations.find((c) => c.id === user.id)
|
||||||
const adminUser = new UserAdmin(
|
const adminUser = new UserAdmin(
|
||||||
user,
|
user,
|
||||||
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
||||||
await hasElopageBuys(user.email),
|
await hasElopageBuys(user.emailContact.email),
|
||||||
emailConfirmationSend,
|
emailConfirmationSend,
|
||||||
)
|
)
|
||||||
return adminUser
|
return adminUser
|
||||||
@ -239,24 +216,39 @@ export class AdminResolver {
|
|||||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<Decimal[]> {
|
): Promise<Decimal[]> {
|
||||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
logger.info(
|
||||||
if (!user) {
|
`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}`)
|
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.')
|
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')
|
throw new Error('Contribution could not be saved, Email is not activated')
|
||||||
}
|
}
|
||||||
const moderator = getUser(context)
|
const moderator = getUser(context)
|
||||||
logger.trace('moderator: ', moderator.id)
|
logger.trace('moderator: ', moderator.id)
|
||||||
const creations = await getUserCreation(user.id)
|
const creations = await getUserCreation(emailContact.userId)
|
||||||
logger.trace('creations', creations)
|
logger.trace('creations:', creations)
|
||||||
const creationDateObj = new Date(creationDate)
|
const creationDateObj = new Date(creationDate)
|
||||||
|
logger.trace('creationDateObj:', creationDateObj)
|
||||||
validateContribution(creations, amount, creationDateObj)
|
validateContribution(creations, amount, creationDateObj)
|
||||||
const contribution = Contribution.create()
|
const contribution = DbContribution.create()
|
||||||
contribution.userId = user.id
|
contribution.userId = emailContact.userId
|
||||||
contribution.amount = amount
|
contribution.amount = amount
|
||||||
contribution.createdAt = new Date()
|
contribution.createdAt = new Date()
|
||||||
contribution.contributionDate = creationDateObj
|
contribution.contributionDate = creationDateObj
|
||||||
@ -266,8 +258,8 @@ export class AdminResolver {
|
|||||||
contribution.contributionStatus = ContributionStatus.PENDING
|
contribution.contributionStatus = ContributionStatus.PENDING
|
||||||
|
|
||||||
logger.trace('contribution to save', contribution)
|
logger.trace('contribution to save', contribution)
|
||||||
await Contribution.save(contribution)
|
await DbContribution.save(contribution)
|
||||||
return getUserCreation(user.id)
|
return getUserCreation(emailContact.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||||
@ -303,29 +295,43 @@ export class AdminResolver {
|
|||||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<AdminUpdateContribution> {
|
): 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) {
|
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) {
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User was deleted (${email})`)
|
||||||
throw new Error(`User was deleted (${email})`)
|
throw new Error(`User was deleted (${email})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const moderator = getUser(context)
|
const moderator = getUser(context)
|
||||||
|
|
||||||
const contributionToUpdate = await Contribution.findOne({
|
const contributionToUpdate = await DbContribution.findOne({
|
||||||
where: { id, confirmedAt: IsNull() },
|
where: { id, confirmedAt: IsNull() },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!contributionToUpdate) {
|
if (!contributionToUpdate) {
|
||||||
|
logger.error('No contribution found to given id.')
|
||||||
throw new Error('No contribution found to given id.')
|
throw new Error('No contribution found to given id.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contributionToUpdate.userId !== user.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')
|
throw new Error('user of the pending contribution and send user does not correspond')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contributionToUpdate.moderatorId === null) {
|
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.')
|
throw new Error('An admin is not allowed to update a user contribution.')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +349,7 @@ export class AdminResolver {
|
|||||||
contributionToUpdate.moderatorId = moderator.id
|
contributionToUpdate.moderatorId = moderator.id
|
||||||
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
|
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
|
||||||
|
|
||||||
await Contribution.save(contributionToUpdate)
|
await DbContribution.save(contributionToUpdate)
|
||||||
const result = new AdminUpdateContribution()
|
const result = new AdminUpdateContribution()
|
||||||
result.amount = amount
|
result.amount = amount
|
||||||
result.memo = contributionToUpdate.memo
|
result.memo = contributionToUpdate.memo
|
||||||
@ -357,39 +363,44 @@ export class AdminResolver {
|
|||||||
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||||
@Query(() => [UnconfirmedContribution])
|
@Query(() => [UnconfirmedContribution])
|
||||||
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
async listUnconfirmedContributions(): Promise<UnconfirmedContribution[]> {
|
||||||
const contributions = await Contribution.find({ where: { confirmedAt: IsNull() } })
|
const contributions = await getConnection()
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('c')
|
||||||
|
.from(DbContribution, 'c')
|
||||||
|
.leftJoinAndSelect('c.messages', 'm')
|
||||||
|
.where({ confirmedAt: IsNull() })
|
||||||
|
.getMany()
|
||||||
|
|
||||||
if (contributions.length === 0) {
|
if (contributions.length === 0) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const userIds = contributions.map((p) => p.userId)
|
const userIds = contributions.map((p) => p.userId)
|
||||||
const userCreations = await getUserCreations(userIds)
|
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) => {
|
return contributions.map((contribution) => {
|
||||||
const user = users.find((u) => u.id === contribution.userId)
|
const user = users.find((u) => u.id === contribution.userId)
|
||||||
const creation = userCreations.find((c) => c.id === contribution.userId)
|
const creation = userCreations.find((c) => c.id === contribution.userId)
|
||||||
|
|
||||||
return {
|
return new UnconfirmedContribution(
|
||||||
id: contribution.id,
|
contribution,
|
||||||
userId: contribution.userId,
|
user,
|
||||||
date: contribution.contributionDate,
|
creation ? creation.creations : FULL_CREATION_AVAILABLE,
|
||||||
memo: contribution.memo,
|
)
|
||||||
amount: contribution.amount,
|
|
||||||
moderator: contribution.moderatorId,
|
|
||||||
firstName: user ? user.firstName : '',
|
|
||||||
lastName: user ? user.lastName : '',
|
|
||||||
email: user ? user.email : '',
|
|
||||||
creation: creation ? creation.creations : FULL_CREATION_AVAILABLE,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||||
const contribution = await Contribution.findOne(id)
|
const contribution = await DbContribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found for given id.')
|
throw new Error('Contribution not found for given id.')
|
||||||
}
|
}
|
||||||
contribution.contributionStatus = ContributionStatus.DELETED
|
contribution.contributionStatus = ContributionStatus.DELETED
|
||||||
@ -404,17 +415,24 @@ export class AdminResolver {
|
|||||||
@Arg('id', () => Int) id: number,
|
@Arg('id', () => Int) id: number,
|
||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const contribution = await Contribution.findOne(id)
|
const contribution = await DbContribution.findOne(id)
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
|
logger.error(`Contribution not found for given id: ${id}`)
|
||||||
throw new Error('Contribution not found to given id.')
|
throw new Error('Contribution not found to given id.')
|
||||||
}
|
}
|
||||||
const moderatorUser = getUser(context)
|
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')
|
throw new Error('Moderator can not confirm own contribution')
|
||||||
|
}
|
||||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
const user = await dbUser.findOneOrFail(
|
||||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
{ 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)
|
const creations = await getUserCreation(contribution.userId, false)
|
||||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||||
|
|
||||||
@ -422,7 +440,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||||
try {
|
try {
|
||||||
const lastTransaction = await queryRunner.manager
|
const lastTransaction = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -462,10 +480,20 @@ export class AdminResolver {
|
|||||||
contribution.confirmedBy = moderatorUser.id
|
contribution.confirmedBy = moderatorUser.id
|
||||||
contribution.transactionId = transaction.id
|
contribution.transactionId = transaction.id
|
||||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
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()
|
await queryRunner.commitTransaction()
|
||||||
logger.info('creation commited successfuly.')
|
logger.info('creation commited successfuly.')
|
||||||
|
sendContributionConfirmedEmail({
|
||||||
|
senderFirstName: moderatorUser.firstName,
|
||||||
|
senderLastName: moderatorUser.lastName,
|
||||||
|
recipientFirstName: user.firstName,
|
||||||
|
recipientLastName: user.lastName,
|
||||||
|
recipientEmail: user.emailContact.email,
|
||||||
|
contributionMemo: contribution.memo,
|
||||||
|
contributionAmount: contribution.amount,
|
||||||
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`Creation was not successful: ${e}`)
|
logger.error(`Creation was not successful: ${e}`)
|
||||||
@ -477,56 +505,64 @@ export class AdminResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
|
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
|
||||||
@Query(() => [Transaction])
|
@Query(() => ContributionListResult)
|
||||||
async creationTransactionList(
|
async creationTransactionList(
|
||||||
@Args()
|
@Args()
|
||||||
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||||
@Arg('userId', () => Int) userId: number,
|
@Arg('userId', () => Int) userId: number,
|
||||||
): Promise<Transaction[]> {
|
): Promise<ContributionListResult> {
|
||||||
const offset = (currentPage - 1) * pageSize
|
const offset = (currentPage - 1) * pageSize
|
||||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
const [contributionResult, count] = await getConnection()
|
||||||
const [userTransactions] = await transactionRepository.findByUserPaged(
|
.createQueryBuilder()
|
||||||
userId,
|
.select('c')
|
||||||
pageSize,
|
.from(DbContribution, 'c')
|
||||||
offset,
|
.leftJoinAndSelect('c.user', 'u')
|
||||||
order,
|
.where(`user_id = ${userId}`)
|
||||||
true,
|
.limit(pageSize)
|
||||||
)
|
.offset(offset)
|
||||||
|
.orderBy('c.created_at', order)
|
||||||
|
.getManyAndCount()
|
||||||
|
|
||||||
const user = await dbUser.findOneOrFail({ id: userId })
|
return new ContributionListResult(
|
||||||
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
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])
|
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const user = await dbUser.findOneOrFail({ email: email })
|
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||||
|
const user = await findUserByEmail(email)
|
||||||
// can be both types: REGISTER and RESET_PASSWORD
|
if (!user) {
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
logger.error(`Could not find User to emailContact: ${email}`)
|
||||||
where: { userId: user.id },
|
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||||
order: { updatedAt: 'DESC' },
|
}
|
||||||
})
|
if (user.deletedAt) {
|
||||||
|
logger.error(`User with emailContact: ${email} is deleted.`)
|
||||||
optInCode = await checkOptInCode(optInCode, user)
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountActivationEmail({
|
const emailSent = await sendAccountActivationEmail({
|
||||||
link: activationLink(optInCode),
|
link: activationLink(emailContact.emailVerificationCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
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
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
// eslint-disable-next-line no-console
|
logger.info(`Account confirmation link: ${activationLink}`)
|
||||||
console.log(`Account confirmation link: ${activationLink}`)
|
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -696,4 +732,73 @@ export class AdminResolver {
|
|||||||
logger.debug(`updateContributionLink successful!`)
|
logger.debug(`updateContributionLink successful!`)
|
||||||
return new ContributionLink(dbContributionLink)
|
return new ContributionLink(dbContributionLink)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_MESSAGE])
|
||||||
|
@Mutation(() => ContributionMessage)
|
||||||
|
async adminCreateContributionMessage(
|
||||||
|
@Args() { contributionId, message }: ContributionMessageArgs,
|
||||||
|
@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('REPEATABLE READ')
|
||||||
|
const contributionMessage = DbContributionMessage.create()
|
||||||
|
try {
|
||||||
|
const contribution = await DbContribution.findOne({
|
||||||
|
where: { id: contributionId },
|
||||||
|
relations: ['user'],
|
||||||
|
})
|
||||||
|
if (!contribution) {
|
||||||
|
throw new Error('Contribution not found')
|
||||||
|
}
|
||||||
|
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
|
||||||
|
contributionMessage.userId = user.id
|
||||||
|
contributionMessage.type = ContributionMessageType.DIALOG
|
||||||
|
contributionMessage.isModerator = true
|
||||||
|
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
|
||||||
|
|
||||||
|
if (
|
||||||
|
contribution.contributionStatus === ContributionStatus.DELETED ||
|
||||||
|
contribution.contributionStatus === ContributionStatus.DENIED ||
|
||||||
|
contribution.contributionStatus === ContributionStatus.PENDING
|
||||||
|
) {
|
||||||
|
contribution.contributionStatus = ContributionStatus.IN_PROGRESS
|
||||||
|
await queryRunner.manager.update(DbContribution, { id: contributionId }, contribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendAddedContributionMessageEmail({
|
||||||
|
senderFirstName: user.firstName,
|
||||||
|
senderLastName: user.lastName,
|
||||||
|
recipientFirstName: contribution.user.firstName,
|
||||||
|
recipientLastName: contribution.user.lastName,
|
||||||
|
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}`)
|
||||||
|
throw new Error(`ContributionMessage was not successful: ${e}`)
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release()
|
||||||
|
}
|
||||||
|
return new ContributionMessage(contributionMessage, user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
351
backend/src/graphql/resolver/ContributionMessageResolver.test.ts
Normal file
351
backend/src/graphql/resolver/ContributionMessageResolver.test.ts
Normal file
@ -0,0 +1,351 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { cleanDB, resetToken, testEnvironment } from '@test/helpers'
|
||||||
|
import { GraphQLError } from 'graphql'
|
||||||
|
import {
|
||||||
|
adminCreateContributionMessage,
|
||||||
|
createContribution,
|
||||||
|
createContributionMessage,
|
||||||
|
} from '@/seeds/graphql/mutations'
|
||||||
|
import { listContributionMessages, login } from '@/seeds/graphql/queries'
|
||||||
|
import { userFactory } from '@/seeds/factory/user'
|
||||||
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
|
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||||
|
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
|
||||||
|
|
||||||
|
jest.mock('@/mailer/sendAddedContributionMessageEmail', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendAddedContributionMessageEmail: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let mutate: any, query: 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()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
await con.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ContributionMessageResolver', () => {
|
||||||
|
describe('adminCreateContributionMessage', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('returns an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: adminCreateContributionMessage,
|
||||||
|
variables: { contributionId: 1, message: 'This is a test message' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
await userFactory(testEnv, peterLustig)
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
result = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: new Date().toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('input not valid', () => {
|
||||||
|
it('throws error when contribution does not exist', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: adminCreateContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: -1,
|
||||||
|
message: 'Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError(
|
||||||
|
'ContributionMessage was not successful: Error: Contribution not found',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error when contribution.userId equals user.id', async () => {
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
const result2 = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: 100.0,
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
creationDate: new Date().toString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: adminCreateContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: result2.data.createContribution.id,
|
||||||
|
message: 'Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError(
|
||||||
|
'ContributionMessage was not successful: Error: Admin can not answer on own contribution',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('valid input', () => {
|
||||||
|
it('creates ContributionMessage', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: adminCreateContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: result.data.createContribution.id,
|
||||||
|
message: 'Admin Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
adminCreateContributionMessage: expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
message: 'Admin Test',
|
||||||
|
type: 'DIALOG',
|
||||||
|
userFirstName: 'Peter',
|
||||||
|
userLastName: 'Lustig',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls sendAddedContributionMessageEmail', async () => {
|
||||||
|
expect(sendAddedContributionMessageEmail).toBeCalledWith({
|
||||||
|
senderFirstName: 'Peter',
|
||||||
|
senderLastName: 'Lustig',
|
||||||
|
recipientFirstName: 'Bibi',
|
||||||
|
recipientLastName: 'Bloxberg',
|
||||||
|
recipientEmail: 'bibi@bloxberg.de',
|
||||||
|
senderEmail: 'peter@lustig.de',
|
||||||
|
contributionMemo: 'Test env contribution',
|
||||||
|
message: 'Admin Test',
|
||||||
|
overviewURL: 'http://localhost/overview',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('createContributionMessage', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('returns an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContributionMessage,
|
||||||
|
variables: { contributionId: 1, message: 'This is a test message' },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('input not valid', () => {
|
||||||
|
it('throws error when contribution does not exist', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: -1,
|
||||||
|
message: 'Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError(
|
||||||
|
'ContributionMessage was not successful: Error: Contribution not found',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error when other user tries to send createContributionMessage', async () => {
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: result.data.createContribution.id,
|
||||||
|
message: 'Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError(
|
||||||
|
'ContributionMessage was not successful: Error: Can not send message to contribution of another user',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('valid input', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates ContributionMessage', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createContributionMessage,
|
||||||
|
variables: {
|
||||||
|
contributionId: result.data.createContribution.id,
|
||||||
|
message: 'User Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
createContributionMessage: expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
message: 'User Test',
|
||||||
|
type: 'DIALOG',
|
||||||
|
userFirstName: 'Bibi',
|
||||||
|
userLastName: 'Bloxberg',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('listContributionMessages', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('returns an error', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: listContributionMessages,
|
||||||
|
variables: { contributionId: 1 },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns a list of contributionmessages', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: listContributionMessages,
|
||||||
|
variables: { contributionId: result.data.createContribution.id },
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listContributionMessages: {
|
||||||
|
count: 2,
|
||||||
|
messages: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
message: 'Admin Test',
|
||||||
|
type: 'DIALOG',
|
||||||
|
userFirstName: 'Peter',
|
||||||
|
userLastName: 'Lustig',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
message: 'User Test',
|
||||||
|
type: 'DIALOG',
|
||||||
|
userFirstName: 'Bibi',
|
||||||
|
userLastName: 'Bloxberg',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
85
backend/src/graphql/resolver/ContributionMessageResolver.ts
Normal file
85
backend/src/graphql/resolver/ContributionMessageResolver.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
|
import { Context, getUser } from '@/server/context'
|
||||||
|
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||||
|
import { Arg, Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'
|
||||||
|
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
||||||
|
import { Contribution } from '@entity/Contribution'
|
||||||
|
import { ContributionMessageType } from '@enum/MessageType'
|
||||||
|
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||||
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
|
import { ContributionMessage, ContributionMessageListResult } from '@model/ContributionMessage'
|
||||||
|
import Paginated from '@arg/Paginated'
|
||||||
|
import { Order } from '@enum/Order'
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class ContributionMessageResolver {
|
||||||
|
@Authorized([RIGHTS.CREATE_CONTRIBUTION_MESSAGE])
|
||||||
|
@Mutation(() => ContributionMessage)
|
||||||
|
async createContributionMessage(
|
||||||
|
@Args() { contributionId, message }: ContributionMessageArgs,
|
||||||
|
@Ctx() context: Context,
|
||||||
|
): Promise<ContributionMessage> {
|
||||||
|
const user = getUser(context)
|
||||||
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
|
await queryRunner.connect()
|
||||||
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
const contributionMessage = DbContributionMessage.create()
|
||||||
|
try {
|
||||||
|
const contribution = await Contribution.findOne({ id: contributionId })
|
||||||
|
if (!contribution) {
|
||||||
|
throw new Error('Contribution not found')
|
||||||
|
}
|
||||||
|
if (contribution.userId !== user.id) {
|
||||||
|
throw new Error('Can not send message to contribution of another user')
|
||||||
|
}
|
||||||
|
|
||||||
|
contributionMessage.contributionId = contributionId
|
||||||
|
contributionMessage.createdAt = new Date()
|
||||||
|
contributionMessage.message = message
|
||||||
|
contributionMessage.userId = user.id
|
||||||
|
contributionMessage.type = ContributionMessageType.DIALOG
|
||||||
|
contributionMessage.isModerator = false
|
||||||
|
await queryRunner.manager.insert(DbContributionMessage, contributionMessage)
|
||||||
|
|
||||||
|
if (contribution.contributionStatus === ContributionStatus.IN_PROGRESS) {
|
||||||
|
contribution.contributionStatus = ContributionStatus.PENDING
|
||||||
|
await queryRunner.manager.update(Contribution, { id: contributionId }, contribution)
|
||||||
|
}
|
||||||
|
await queryRunner.commitTransaction()
|
||||||
|
} catch (e) {
|
||||||
|
await queryRunner.rollbackTransaction()
|
||||||
|
logger.error(`ContributionMessage was not successful: ${e}`)
|
||||||
|
throw new Error(`ContributionMessage was not successful: ${e}`)
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release()
|
||||||
|
}
|
||||||
|
return new ContributionMessage(contributionMessage, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES])
|
||||||
|
@Query(() => ContributionMessageListResult)
|
||||||
|
async listContributionMessages(
|
||||||
|
@Arg('contributionId') contributionId: number,
|
||||||
|
@Args()
|
||||||
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||||
|
): Promise<ContributionMessageListResult> {
|
||||||
|
const [contributionMessages, count] = await getConnection()
|
||||||
|
.createQueryBuilder()
|
||||||
|
.select('cm')
|
||||||
|
.from(DbContributionMessage, 'cm')
|
||||||
|
.leftJoinAndSelect('cm.user', 'u')
|
||||||
|
.where({ contributionId: contributionId })
|
||||||
|
.orderBy('cm.createdAt', order)
|
||||||
|
.limit(pageSize)
|
||||||
|
.offset((currentPage - 1) * pageSize)
|
||||||
|
.getManyAndCount()
|
||||||
|
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
messages: contributionMessages.map(
|
||||||
|
(message) => new ContributionMessage(message, message.user),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,7 +11,6 @@ import { ContributionType } from '@enum/ContributionType'
|
|||||||
import { ContributionStatus } from '@enum/ContributionStatus'
|
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||||
import { Contribution, ContributionListResult } from '@model/Contribution'
|
import { Contribution, ContributionListResult } from '@model/Contribution'
|
||||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||||
import { User } from '@model/User'
|
|
||||||
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
|
|
||||||
@ -90,19 +89,23 @@ export class ContributionResolver {
|
|||||||
userId: number
|
userId: number
|
||||||
confirmedBy?: FindOperator<number> | null
|
confirmedBy?: FindOperator<number> | null
|
||||||
} = { userId: user.id }
|
} = { userId: user.id }
|
||||||
|
|
||||||
if (filterConfirmed) where.confirmedBy = IsNull()
|
if (filterConfirmed) where.confirmedBy = IsNull()
|
||||||
const [contributions, count] = await dbContribution.findAndCount({
|
|
||||||
where,
|
const [contributions, count] = await getConnection()
|
||||||
order: {
|
.createQueryBuilder()
|
||||||
createdAt: order,
|
.select('c')
|
||||||
},
|
.from(dbContribution, 'c')
|
||||||
withDeleted: true,
|
.leftJoinAndSelect('c.messages', 'm')
|
||||||
skip: (currentPage - 1) * pageSize,
|
.where(where)
|
||||||
take: pageSize,
|
.orderBy('c.createdAt', order)
|
||||||
})
|
.limit(pageSize)
|
||||||
|
.offset((currentPage - 1) * pageSize)
|
||||||
|
.getManyAndCount()
|
||||||
|
|
||||||
return new ContributionListResult(
|
return new ContributionListResult(
|
||||||
count,
|
count,
|
||||||
contributions.map((contribution) => new Contribution(contribution, new User(user))),
|
contributions.map((contribution) => new Contribution(contribution, user)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,9 +126,7 @@ export class ContributionResolver {
|
|||||||
.getManyAndCount()
|
.getManyAndCount()
|
||||||
return new ContributionListResult(
|
return new ContributionListResult(
|
||||||
count,
|
count,
|
||||||
dbContributions.map(
|
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||||
(contribution) => new Contribution(contribution, new User(contribution.user)),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export class GdtResolver {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resultGDT = await apiGet(
|
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) {
|
if (!resultGDT.success) {
|
||||||
throw new Error(resultGDT.data)
|
throw new Error(resultGDT.data)
|
||||||
@ -37,7 +37,7 @@ export class GdtResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
try {
|
try {
|
||||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||||
email: user.email,
|
email: user.emailContact.email,
|
||||||
})
|
})
|
||||||
if (!resultGDTSum.success) {
|
if (!resultGDTSum.success) {
|
||||||
throw new Error('Call not successful')
|
throw new Error('Call not successful')
|
||||||
|
|||||||
@ -7,49 +7,48 @@ import { getConnection } from '@dbTools/typeorm'
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
|
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class StatisticsResolver {
|
export class StatisticsResolver {
|
||||||
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
|
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
|
||||||
@Query(() => CommunityStatistics)
|
@Query(() => CommunityStatistics)
|
||||||
async communityStatistics(): Promise<CommunityStatistics> {
|
async communityStatistics(): Promise<CommunityStatistics> {
|
||||||
const allUsers = await DbUser.find({ withDeleted: true })
|
const allUsers = await DbUser.count({ withDeleted: true })
|
||||||
|
const totalUsers = await DbUser.count()
|
||||||
let totalUsers = 0
|
const deletedUsers = allUsers - totalUsers
|
||||||
let activeUsers = 0
|
|
||||||
let deletedUsers = 0
|
|
||||||
|
|
||||||
let totalGradidoAvailable: Decimal = new Decimal(0)
|
let totalGradidoAvailable: Decimal = new Decimal(0)
|
||||||
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
|
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
|
||||||
|
|
||||||
const receivedCallDate = new Date()
|
const receivedCallDate = new Date()
|
||||||
|
|
||||||
for (let i = 0; i < allUsers.length; i++) {
|
|
||||||
if (allUsers[i].deletedAt) {
|
|
||||||
deletedUsers++
|
|
||||||
} else {
|
|
||||||
totalUsers++
|
|
||||||
const lastTransaction = await DbTransaction.findOne({
|
|
||||||
where: { userId: allUsers[i].id },
|
|
||||||
order: { balanceDate: 'DESC' },
|
|
||||||
})
|
|
||||||
if (lastTransaction) {
|
|
||||||
activeUsers++
|
|
||||||
const decay = calculateDecay(
|
|
||||||
lastTransaction.balance,
|
|
||||||
lastTransaction.balanceDate,
|
|
||||||
receivedCallDate,
|
|
||||||
)
|
|
||||||
if (decay) {
|
|
||||||
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
|
|
||||||
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
|
|
||||||
|
const lastUserTransactions = await queryRunner.manager
|
||||||
|
.createQueryBuilder(DbUser, 'user')
|
||||||
|
.select('transaction.balance', 'balance')
|
||||||
|
.addSelect('transaction.balance_date', 'balanceDate')
|
||||||
|
.innerJoin(DbTransaction, 'transaction', 'user.id = transaction.user_id')
|
||||||
|
.where(
|
||||||
|
`transaction.balance_date = (SELECT MAX(t.balance_date) FROM transactions AS t WHERE t.user_id = user.id)`,
|
||||||
|
)
|
||||||
|
.orderBy('transaction.balance_date', 'DESC')
|
||||||
|
.addOrderBy('transaction.id', 'DESC')
|
||||||
|
.getRawMany()
|
||||||
|
|
||||||
|
const activeUsers = lastUserTransactions.length
|
||||||
|
|
||||||
|
lastUserTransactions.forEach(({ balance, balanceDate }) => {
|
||||||
|
const decay = calculateDecay(new Decimal(balance), new Date(balanceDate), receivedCallDate)
|
||||||
|
if (decay) {
|
||||||
|
totalGradidoAvailable = totalGradidoAvailable.plus(decay.balance.toString())
|
||||||
|
totalGradidoUnbookedDecayed = totalGradidoUnbookedDecayed.plus(decay.decay.toString())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const { totalGradidoCreated } = await queryRunner.manager
|
const { totalGradidoCreated } = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select('SUM(transaction.amount) AS totalGradidoCreated')
|
.select('SUM(transaction.amount) AS totalGradidoCreated')
|
||||||
|
|||||||
@ -178,7 +178,7 @@ export class TransactionLinkResolver {
|
|||||||
logger.info('redeem contribution link...')
|
logger.info('redeem contribution link...')
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('SERIALIZABLE')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
const contributionLink = await queryRunner.manager
|
const contributionLink = await queryRunner.manager
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
@ -283,7 +283,10 @@ export class TransactionLinkResolver {
|
|||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
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) {
|
if (user.id === linkedUser.id) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new Error('Cannot redeem own transaction link.')
|
||||||
|
|||||||
@ -35,6 +35,8 @@ import Decimal from 'decimal.js-light'
|
|||||||
|
|
||||||
import { BalanceResolver } from './BalanceResolver'
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
|
import { findUserByEmail } from './UserResolver'
|
||||||
|
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
@ -78,7 +80,7 @@ export const executeTransaction = async (
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
logger.debug(`open Transaction to write...`)
|
logger.debug(`open Transaction to write...`)
|
||||||
try {
|
try {
|
||||||
// transaction
|
// transaction
|
||||||
@ -148,12 +150,24 @@ export const executeTransaction = async (
|
|||||||
senderLastName: sender.lastName,
|
senderLastName: sender.lastName,
|
||||||
recipientFirstName: recipient.firstName,
|
recipientFirstName: recipient.firstName,
|
||||||
recipientLastName: recipient.lastName,
|
recipientLastName: recipient.lastName,
|
||||||
email: recipient.email,
|
email: recipient.emailContact.email,
|
||||||
senderEmail: sender.email,
|
senderEmail: sender.emailContact.email,
|
||||||
amount,
|
amount,
|
||||||
memo,
|
|
||||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
})
|
})
|
||||||
|
if (transactionLink) {
|
||||||
|
await sendTransactionLinkRedeemedEmail({
|
||||||
|
senderFirstName: recipient.firstName,
|
||||||
|
senderLastName: recipient.lastName,
|
||||||
|
recipientFirstName: sender.firstName,
|
||||||
|
recipientLastName: sender.lastName,
|
||||||
|
email: sender.emailContact.email,
|
||||||
|
senderEmail: recipient.emailContact.email,
|
||||||
|
amount,
|
||||||
|
memo,
|
||||||
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
|
})
|
||||||
|
}
|
||||||
logger.info(`finished executeTransaction successfully`)
|
logger.info(`finished executeTransaction successfully`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -171,7 +185,7 @@ export class TransactionResolver {
|
|||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
|
|
||||||
logger.addContext('user', user.id)
|
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
|
// find current balance
|
||||||
const lastTransaction = await dbTransaction.findOne(
|
const lastTransaction = await dbTransaction.findOne(
|
||||||
@ -293,16 +307,25 @@ export class TransactionResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate recipient user
|
// 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 })
|
||||||
if (!recipientUser) {
|
if (!recipientUser) {
|
||||||
logger.error(`recipient not known: email=${email}`)
|
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||||
throw new Error('recipient not known')
|
throw new Error('unknown recipient')
|
||||||
}
|
}
|
||||||
if (recipientUser.deletedAt) {
|
if (recipientUser.deletedAt) {
|
||||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account was deleted')
|
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}`)
|
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||||
throw new Error('The recipient account is not activated')
|
throw new Error('The recipient account is not activated')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* 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 { userFactory } from '@/seeds/factory/user'
|
||||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||||
import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
import {
|
||||||
|
createUser,
|
||||||
|
setPassword,
|
||||||
|
forgotPassword,
|
||||||
|
updateUserInfos,
|
||||||
|
createContribution,
|
||||||
|
confirmContribution,
|
||||||
|
} from '@/seeds/graphql/mutations'
|
||||||
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||||
import { GraphQLError } from 'graphql'
|
import { GraphQLError } from 'graphql'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
@ -15,13 +21,19 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi
|
|||||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { printTimeDuration, activationLink } from './UserResolver'
|
import { printTimeDuration, activationLink } from './UserResolver'
|
||||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||||
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||||
import { ContributionLink } from '@model/ContributionLink'
|
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 { logger } from '@test/testSetup'
|
||||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
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'
|
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
|
|
||||||
@ -82,7 +94,7 @@ describe('UserResolver', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailVerificationCode: string
|
||||||
let user: User[]
|
let user: User[]
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -101,11 +113,11 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('valid input data', () => {
|
describe('valid input data', () => {
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
// let loginEmailOptIn: LoginEmailOptIn[]
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
user = await User.find()
|
user = await User.find({ relations: ['emailContact'] })
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
// loginEmailOptIn = await LoginEmailOptIn.find()
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = user[0].emailContact.emailVerificationCode.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('filling all tables', () => {
|
describe('filling all tables', () => {
|
||||||
@ -115,15 +127,16 @@ describe('UserResolver', () => {
|
|||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
gradidoID: expect.any(String),
|
gradidoID: expect.any(String),
|
||||||
alias: null,
|
alias: null,
|
||||||
email: 'peter@lustig.de',
|
emailContact: expect.any(UserContact), // 'peter@lustig.de',
|
||||||
|
emailId: expect.any(Number),
|
||||||
firstName: 'Peter',
|
firstName: 'Peter',
|
||||||
lastName: 'Lustig',
|
lastName: 'Lustig',
|
||||||
password: '0',
|
password: '0',
|
||||||
pubKey: null,
|
pubKey: null,
|
||||||
privKey: null,
|
privKey: null,
|
||||||
emailHash: expect.any(Buffer),
|
// emailHash: expect.any(Buffer),
|
||||||
createdAt: expect.any(Date),
|
createdAt: expect.any(Date),
|
||||||
emailChecked: false,
|
// emailChecked: false,
|
||||||
passphrase: expect.any(String),
|
passphrase: expect.any(String),
|
||||||
language: 'de',
|
language: 'de',
|
||||||
isAdmin: null,
|
isAdmin: null,
|
||||||
@ -139,18 +152,21 @@ describe('UserResolver', () => {
|
|||||||
expect(verUUID).toEqual(4)
|
expect(verUUID).toEqual(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates an email optin', () => {
|
it('creates an email contact', () => {
|
||||||
expect(loginEmailOptIn).toEqual([
|
expect(user[0].emailContact).toEqual({
|
||||||
{
|
id: expect.any(Number),
|
||||||
id: expect.any(Number),
|
type: UserContactType.USER_CONTACT_EMAIL,
|
||||||
userId: user[0].id,
|
userId: user[0].id,
|
||||||
verificationCode: expect.any(String),
|
email: 'peter@lustig.de',
|
||||||
emailOptInTypeId: 1,
|
emailChecked: false,
|
||||||
createdAt: expect.any(Date),
|
emailVerificationCode: expect.any(String),
|
||||||
resendCount: 0,
|
emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER,
|
||||||
updatedAt: expect.any(Date),
|
emailResendCount: 0,
|
||||||
},
|
phone: null,
|
||||||
])
|
createdAt: expect.any(Date),
|
||||||
|
deletedAt: null,
|
||||||
|
updatedAt: null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -159,7 +175,7 @@ describe('UserResolver', () => {
|
|||||||
it('sends an account activation email', () => {
|
it('sends an account activation email', () => {
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailOptIn,
|
emailVerificationCode,
|
||||||
).replace(/{code}/g, '')
|
).replace(/{code}/g, '')
|
||||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||||
link: activationLink,
|
link: activationLink,
|
||||||
@ -169,6 +185,15 @@ describe('UserResolver', () => {
|
|||||||
duration: expect.any(String),
|
duration: expect.any(String),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stores the send confirmation event in the database', () => {
|
||||||
|
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
|
||||||
|
userId: user[0].id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('email already exists', () => {
|
describe('email already exists', () => {
|
||||||
@ -208,13 +233,13 @@ describe('UserResolver', () => {
|
|||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
||||||
})
|
})
|
||||||
await expect(User.find()).resolves.toEqual(
|
await expect(
|
||||||
expect.arrayContaining([
|
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
|
||||||
expect.objectContaining({
|
).resolves.toEqual(
|
||||||
email: 'bibi@bloxberg.de',
|
expect.objectContaining({
|
||||||
language: 'de',
|
email: 'bibi@bloxberg.de',
|
||||||
}),
|
user: expect.objectContaining({ language: 'de' }),
|
||||||
]),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -225,10 +250,12 @@ describe('UserResolver', () => {
|
|||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
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.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email: 'raeuber@hotzenplotz.de',
|
emailContact: expect.objectContaining({
|
||||||
|
email: 'raeuber@hotzenplotz.de',
|
||||||
|
}),
|
||||||
publisherId: null,
|
publisherId: null,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@ -237,37 +264,157 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('redeem codes', () => {
|
describe('redeem codes', () => {
|
||||||
|
let result: any
|
||||||
|
let link: ContributionLink
|
||||||
|
|
||||||
describe('contribution link', () => {
|
describe('contribution link', () => {
|
||||||
let link: ContributionLink
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// activate account of admin Peter Lustig
|
// activate account of admin Peter Lustig
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// make Peter Lustig Admin
|
// make Peter Lustig Admin
|
||||||
const peter = await User.findOneOrFail({ id: user[0].id })
|
const peter = await User.findOneOrFail({ id: user[0].id })
|
||||||
peter.isAdmin = new Date()
|
peter.isAdmin = new Date()
|
||||||
await peter.save()
|
await peter.save()
|
||||||
|
|
||||||
|
// date statement
|
||||||
|
const actualDate = new Date()
|
||||||
|
const futureDate = new Date() // Create a future day from the executed day
|
||||||
|
futureDate.setDate(futureDate.getDate() + 1)
|
||||||
|
|
||||||
// factory logs in as Peter Lustig
|
// factory logs in as Peter Lustig
|
||||||
link = await contributionLinkFactory(testEnv, {
|
link = await contributionLinkFactory(testEnv, {
|
||||||
name: 'Dokumenta 2022',
|
name: 'Dokumenta 2022',
|
||||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||||
amount: 200,
|
amount: 200,
|
||||||
validFrom: new Date(2022, 5, 18),
|
validFrom: actualDate,
|
||||||
validTo: new Date(2022, 8, 25),
|
validTo: futureDate,
|
||||||
})
|
})
|
||||||
|
|
||||||
resetToken()
|
resetToken()
|
||||||
await mutate({
|
result = await mutate({
|
||||||
mutation: createUser,
|
mutation: createUser,
|
||||||
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
it('sets the contribution link id', async () => {
|
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({
|
expect.objectContaining({
|
||||||
contributionLinkId: link.id,
|
user: expect.objectContaining({
|
||||||
|
contributionLinkId: link.id,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stores the account activated event in the database', () => {
|
||||||
|
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.ACTIVATE_ACCOUNT,
|
||||||
|
userId: user[0].id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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 query({ query: 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 query({ query: login, variables: peterData })
|
||||||
|
|
||||||
|
// confirm the contribution
|
||||||
|
contribution = await mutate({
|
||||||
|
mutation: confirmContribution,
|
||||||
|
variables: { id: contribution.data.createContribution.id },
|
||||||
|
})
|
||||||
|
|
||||||
|
// login as user bob
|
||||||
|
bob = await query({ query: 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,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -282,7 +429,7 @@ describe('UserResolver', () => {
|
|||||||
email: 'peter@lustig.de',
|
email: 'peter@lustig.de',
|
||||||
amount: 19.99,
|
amount: 19.99,
|
||||||
memo: `Kein Trick, keine Zauberrei,
|
memo: `Kein Trick, keine Zauberrei,
|
||||||
bei Gradidio sei dabei!`,
|
bei Gradidio sei dabei!`,
|
||||||
})
|
})
|
||||||
const transactionLink = await TransactionLink.findOneOrFail()
|
const transactionLink = await TransactionLink.findOneOrFail()
|
||||||
resetToken()
|
resetToken()
|
||||||
@ -291,14 +438,14 @@ bei Gradidio sei dabei!`,
|
|||||||
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the referrer id to Peter Lustigs id', async () => {
|
it('sets the referrer id to Peter Lustigs id', async () => {
|
||||||
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
await expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual(expect.objectContaining({
|
||||||
referrerId: user[0].id,
|
referrerId: user[0].id,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
*/
|
*/
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -313,20 +460,23 @@ bei Gradidio sei dabei!`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result: any
|
let result: any
|
||||||
let emailOptIn: string
|
let emailVerificationCode: string
|
||||||
|
|
||||||
describe('valid optin code and valid password', () => {
|
describe('valid optin code and valid password', () => {
|
||||||
let newUser: any
|
let newUser: User
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||||
result = await mutate({
|
result = await mutate({
|
||||||
mutation: setPassword,
|
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 () => {
|
afterAll(async () => {
|
||||||
@ -334,11 +484,11 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sets email checked to true', () => {
|
it('sets email checked to true', () => {
|
||||||
expect(newUser[0].emailChecked).toBeTruthy()
|
expect(newUser.emailContact.emailChecked).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates the password', () => {
|
it('updates the password', () => {
|
||||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
expect(newUser.password).toEqual('3917921995996627700')
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -360,11 +510,11 @@ bei Gradidio sei dabei!`,
|
|||||||
describe('no valid password', () => {
|
describe('no valid password', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||||
result = await mutate({
|
result = await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { code: emailOptIn, password: 'not-valid' },
|
variables: { code: emailVerificationCode, password: 'not-valid' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -383,6 +533,10 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Password entered is lexically invalid')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('no valid optin code', () => {
|
describe('no valid optin code', () => {
|
||||||
@ -405,6 +559,10 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Could not login with emailVerificationCode')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -423,6 +581,7 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
describe('no users in database', () => {
|
describe('no users in database', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
result = await query({ query: login, variables })
|
result = await query({ query: login, variables })
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -433,6 +592,12 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'UserContact with email=bibi@bloxberg.de does not exists',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user is in database and correct login data', () => {
|
describe('user is in database and correct login data', () => {
|
||||||
@ -475,6 +640,7 @@ bei Gradidio sei dabei!`,
|
|||||||
describe('user is in database and wrong password', () => {
|
describe('user is in database and wrong password', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
|
result = await query({ query: login, variables: { ...variables, password: 'wrong' } })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -482,14 +648,16 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error', () => {
|
it('returns an error', () => {
|
||||||
expect(
|
expect(result).toEqual(
|
||||||
query({ query: login, variables: { ...variables, password: 'wrong' } }),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('No user with this credentials')],
|
errors: [new GraphQLError('No user with this credentials')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('The User has no valid credentials.')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -562,6 +730,8 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
|
let user: User[]
|
||||||
|
|
||||||
const variables = {
|
const variables = {
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
password: 'Aa12345_',
|
password: 'Aa12345_',
|
||||||
@ -569,6 +739,7 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await query({ query: login, variables })
|
await query({ query: login, variables })
|
||||||
|
user = await User.find()
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@ -595,52 +766,83 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('stores the login event in the database', () => {
|
||||||
|
expect(EventProtocol.find()).resolves.toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: EventProtocolType.LOGIN,
|
||||||
|
userId: user[0].id,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('forgotPassword', () => {
|
describe('forgotPassword', () => {
|
||||||
const variables = { email: 'bibi@bloxberg.de' }
|
const variables = { email: 'bibi@bloxberg.de' }
|
||||||
|
const emailCodeRequestTime = CONFIG.EMAIL_CODE_REQUEST_TIME
|
||||||
|
|
||||||
describe('user is not in DB', () => {
|
describe('user is not in DB', () => {
|
||||||
it('returns true', async () => {
|
describe('duration not expired', () => {
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
it('returns true', async () => {
|
||||||
expect.objectContaining({
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
data: {
|
expect.objectContaining({
|
||||||
forgotPassword: true,
|
data: {
|
||||||
},
|
forgotPassword: true,
|
||||||
}),
|
},
|
||||||
)
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('user exists in DB', () => {
|
describe('user exists in DB', () => {
|
||||||
let result: any
|
let emailContact: UserContact
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
await resetEntity(LoginEmailOptIn)
|
// await resetEntity(LoginEmailOptIn)
|
||||||
result = await mutate({ mutation: forgotPassword, variables })
|
emailContact = await UserContact.findOneOrFail(variables)
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await cleanDB()
|
await cleanDB()
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns true', async () => {
|
describe('duration not expired', () => {
|
||||||
await expect(result).toEqual(
|
it('returns true', async () => {
|
||||||
expect.objectContaining({
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
data: {
|
expect.objectContaining({
|
||||||
forgotPassword: true,
|
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', () => {
|
it('sends reset password email', () => {
|
||||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||||
link: activationLink(loginEmailOptIn[0]),
|
link: activationLink(emailContact.emailVerificationCode),
|
||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
email: 'bibi@bloxberg.de',
|
email: 'bibi@bloxberg.de',
|
||||||
@ -650,22 +852,27 @@ bei Gradidio sei dabei!`,
|
|||||||
|
|
||||||
describe('request reset password again', () => {
|
describe('request reset password again', () => {
|
||||||
it('thows an error', async () => {
|
it('thows an error', async () => {
|
||||||
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes minutes ago`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('queryOptIn', () => {
|
describe('queryOptIn', () => {
|
||||||
let loginEmailOptIn: LoginEmailOptIn[]
|
let emailContact: UserContact
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await userFactory(testEnv, bibiBloxberg)
|
await userFactory(testEnv, bibiBloxberg)
|
||||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -680,8 +887,8 @@ bei Gradidio sei dabei!`,
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
// keep Whitspace in error message!
|
// keep Whitspace in error message!
|
||||||
new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
|
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
|
||||||
"verificationCode": "not-valid"
|
"emailVerificationCode": "not-valid"
|
||||||
}`),
|
}`),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -694,7 +901,7 @@ bei Gradidio sei dabei!`,
|
|||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: queryOptIn,
|
query: queryOptIn,
|
||||||
variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
|
variables: { optIn: emailContact.emailVerificationCode.toString() },
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@ -766,7 +973,7 @@ bei Gradidio sei dabei!`,
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('language is not valid', () => {
|
describe('language is not valid', () => {
|
||||||
it('thows an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: updateUserInfos,
|
mutation: updateUserInfos,
|
||||||
@ -780,6 +987,10 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(`"not-valid" isn't a valid language`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('password', () => {
|
describe('password', () => {
|
||||||
@ -799,6 +1010,10 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(`Old password is invalid`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('invalid new password', () => {
|
describe('invalid new password', () => {
|
||||||
@ -821,6 +1036,10 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error found', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('newPassword does not fullfil the rules')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('correct old and new password', () => {
|
describe('correct old and new password', () => {
|
||||||
@ -840,7 +1059,7 @@ bei Gradidio sei dabei!`,
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('can login wtih new password', async () => {
|
it('can login with new password', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: login,
|
query: login,
|
||||||
@ -860,7 +1079,7 @@ bei Gradidio sei dabei!`,
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cannot login wtih old password', async () => {
|
it('cannot login with old password', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: login,
|
query: login,
|
||||||
@ -875,6 +1094,10 @@ bei Gradidio sei dabei!`,
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('The User has no valid credentials.')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
import { User as DbUser } from '@entity/User'
|
import { User as DbUser } from '@entity/User'
|
||||||
|
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||||
import { communityDbUser } from '@/util/communityUser'
|
import { communityDbUser } from '@/util/communityUser'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||||
@ -16,7 +16,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
|||||||
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
||||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||||
import { OptInType } from '@enum/OptInType'
|
import { OptInType } from '@enum/OptInType'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||||
@ -29,9 +28,12 @@ import {
|
|||||||
EventLogin,
|
EventLogin,
|
||||||
EventRedeemRegister,
|
EventRedeemRegister,
|
||||||
EventRegister,
|
EventRegister,
|
||||||
|
EventSendAccountMultiRegistrationEmail,
|
||||||
EventSendConfirmationEmail,
|
EventSendConfirmationEmail,
|
||||||
|
EventActivateAccount,
|
||||||
} from '@/event/Event'
|
} from '@/event/Event'
|
||||||
import { getUserCreation } from './util/creations'
|
import { getUserCreation } from './util/creations'
|
||||||
|
import { UserContactType } from '../enum/UserContactType'
|
||||||
import { UserRepository } from '@/typeorm/repository/User'
|
import { UserRepository } from '@/typeorm/repository/User'
|
||||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
@ -146,6 +148,7 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
|
|||||||
return [encryptionKeyHash, encryptionKey]
|
return [encryptionKeyHash, encryptionKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
const getEmailHash = (email: string): Buffer => {
|
const getEmailHash = (email: string): Buffer => {
|
||||||
logger.trace('getEmailHash...')
|
logger.trace('getEmailHash...')
|
||||||
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
||||||
@ -153,6 +156,7 @@ const getEmailHash = (email: string): Buffer => {
|
|||||||
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
||||||
return emailHash
|
return emailHash
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||||
logger.trace('SecretKeyCryptographyEncrypt...')
|
logger.trace('SecretKeyCryptographyEncrypt...')
|
||||||
@ -177,6 +181,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
|||||||
return message
|
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 => {
|
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||||
logger.trace('newEmailOptIn...')
|
logger.trace('newEmailOptIn...')
|
||||||
const emailOptIn = new LoginEmailOptIn()
|
const emailOptIn = new LoginEmailOptIn()
|
||||||
@ -186,7 +203,8 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
|||||||
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||||
return emailOptIn
|
return emailOptIn
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
/*
|
||||||
// needed by AdminResolver
|
// needed by AdminResolver
|
||||||
// checks if given code exists and can be resent
|
// checks if given code exists and can be resent
|
||||||
// if optIn does not exits, it is created
|
// if optIn does not exits, it is created
|
||||||
@ -226,10 +244,44 @@ export const checkOptInCode = async (
|
|||||||
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
||||||
return optInCode
|
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 => {
|
export const activationLink = (verificationCode: BigInt): string => {
|
||||||
logger.debug(`activationLink(${LoginEmailOptIn})...`)
|
logger.debug(`activationLink(${verificationCode})...`)
|
||||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
const newGradidoID = async (): Promise<string> => {
|
const newGradidoID = async (): Promise<string> => {
|
||||||
@ -272,15 +324,12 @@ export class UserResolver {
|
|||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
const dbUser = await findUserByEmail(email)
|
||||||
logger.error(`User with email=${email} does not exists`)
|
|
||||||
throw new Error('No user with this credentials')
|
|
||||||
})
|
|
||||||
if (dbUser.deletedAt) {
|
if (dbUser.deletedAt) {
|
||||||
logger.error('The User was permanently deleted in database.')
|
logger.error('The User was permanently deleted in database.')
|
||||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
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.')
|
logger.error('The Users email is not validate yet.')
|
||||||
throw new Error('User email not validated')
|
throw new Error('User email not validated')
|
||||||
}
|
}
|
||||||
@ -305,7 +354,7 @@ export class UserResolver {
|
|||||||
logger.debug('login credentials valid...')
|
logger.debug('login credentials valid...')
|
||||||
|
|
||||||
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
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
|
// Elopage Status & Stored PublisherId
|
||||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||||
@ -323,7 +372,7 @@ export class UserResolver {
|
|||||||
const ev = new EventLogin()
|
const ev = new EventLogin()
|
||||||
ev.userId = user.id
|
ev.userId = user.id
|
||||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||||
logger.info('successful Login:' + user)
|
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,66 +401,72 @@ export class UserResolver {
|
|||||||
)
|
)
|
||||||
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||||
// default int publisher_id = 0;
|
// default int publisher_id = 0;
|
||||||
|
const event = new Event()
|
||||||
|
|
||||||
// Validate Language (no throw)
|
// Validate Language (no throw)
|
||||||
if (!language || !isLanguage(language)) {
|
if (!language || !isLanguage(language)) {
|
||||||
language = DEFAULT_LANGUAGE
|
language = DEFAULT_LANGUAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate email unique
|
// check if user with email still exists?
|
||||||
email = email.trim().toLowerCase()
|
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
|
if (await checkEmailExists(email)) {
|
||||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
const foundUser = await findUserByEmail(email)
|
||||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||||
|
|
||||||
if (userFound) {
|
if (foundUser) {
|
||||||
// ATTENTION: this logger-message will be exactly expected during tests
|
// ATTENTION: this logger-message will be exactly expected during tests
|
||||||
logger.info(`User already exists with this email=${email}`)
|
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.
|
// 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)
|
const user = new User(communityDbUser)
|
||||||
user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in?
|
user.id = 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.gradidoID = uuidv4()
|
||||||
user.email = email
|
user.email = email
|
||||||
user.firstName = firstName
|
user.firstName = firstName
|
||||||
user.lastName = lastName
|
user.lastName = lastName
|
||||||
user.language = language
|
user.language = language
|
||||||
user.publisherId = publisherId
|
user.publisherId = publisherId
|
||||||
logger.debug('partly faked user=' + user)
|
logger.debug('partly faked user=' + user)
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendAccountMultiRegistrationEmail({
|
const emailSent = await sendAccountMultiRegistrationEmail({
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||||
/* uncomment this, when you need the activation link on the console */
|
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||||
// In case EMails are disabled log the activation link for the user
|
eventProtocol.writeEvent(
|
||||||
if (!emailSent) {
|
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||||
logger.debug(`Email not send!`)
|
)
|
||||||
|
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 passphrase = PassphraseGenerate()
|
||||||
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
const emailHash = getEmailHash(email)
|
// const emailHash = getEmailHash(email)
|
||||||
const gradidoID = await newGradidoID()
|
const gradidoID = await newGradidoID()
|
||||||
|
|
||||||
const eventRegister = new EventRegister()
|
const eventRegister = new EventRegister()
|
||||||
const eventRedeemRegister = new EventRedeemRegister()
|
const eventRedeemRegister = new EventRedeemRegister()
|
||||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||||
const dbUser = new DbUser()
|
|
||||||
|
let dbUser = new DbUser()
|
||||||
dbUser.gradidoID = gradidoID
|
dbUser.gradidoID = gradidoID
|
||||||
dbUser.email = email
|
|
||||||
dbUser.firstName = firstName
|
dbUser.firstName = firstName
|
||||||
dbUser.lastName = lastName
|
dbUser.lastName = lastName
|
||||||
dbUser.emailHash = emailHash
|
|
||||||
dbUser.language = language
|
dbUser.language = language
|
||||||
dbUser.publisherId = publisherId
|
dbUser.publisherId = publisherId
|
||||||
dbUser.passphrase = passphrase.join(' ')
|
dbUser.passphrase = passphrase.join(' ')
|
||||||
@ -442,25 +497,38 @@ export class UserResolver {
|
|||||||
// loginUser.pubKey = keyPair[0]
|
// loginUser.pubKey = keyPair[0]
|
||||||
// loginUser.privKey = encryptedPrivkey
|
// loginUser.privKey = encryptedPrivkey
|
||||||
|
|
||||||
const event = new Event()
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
logger.error('Error while saving dbUser', error)
|
logger.error('Error while saving dbUser', error)
|
||||||
throw new Error('error saving user')
|
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)
|
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||||
logger.error('Error while saving emailOptIn', error)
|
logger.error('Error while saving emailOptIn', error)
|
||||||
throw new Error('error saving email opt in')
|
throw new Error('error saving email opt in')
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailOptIn.verificationCode.toString(),
|
emailContact.emailVerificationCode.toString(),
|
||||||
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
@ -475,8 +543,6 @@ export class UserResolver {
|
|||||||
eventSendConfirmEmail.userId = dbUser.id
|
eventSendConfirmEmail.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
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) {
|
if (!emailSent) {
|
||||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||||
}
|
}
|
||||||
@ -493,10 +559,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
eventRedeemRegister.userId = dbUser.id
|
eventRedeemRegister.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||||
} else {
|
} else {
|
||||||
eventRegister.userId = dbUser.id
|
eventRegister.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||||
}
|
}
|
||||||
|
|
||||||
return new User(dbUser)
|
return new User(dbUser)
|
||||||
@ -507,22 +573,29 @@ export class UserResolver {
|
|||||||
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
||||||
logger.info(`forgotPassword(${email})...`)
|
logger.info(`forgotPassword(${email})...`)
|
||||||
email = email.trim().toLowerCase()
|
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) {
|
if (!user) {
|
||||||
logger.warn(`no user found with ${email}`)
|
logger.warn(`no user found with ${email}`)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// can be both types: REGISTER and RESET_PASSWORD
|
// can be both types: REGISTER and RESET_PASSWORD
|
||||||
let optInCode = await LoginEmailOptIn.findOne({
|
// let optInCode = await LoginEmailOptIn.findOne({
|
||||||
userId: user.id,
|
// 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)
|
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||||
logger.info(`optInCode for ${email}=${optInCode}`)
|
logger.info(`optInCode for ${email}=${dbUserContact}`)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emailSent = await sendResetPasswordEmailMailer({
|
const emailSent = await sendResetPasswordEmailMailer({
|
||||||
link: activationLink(optInCode),
|
link: activationLink(dbUserContact.emailVerificationCode),
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email,
|
email,
|
||||||
@ -532,7 +605,7 @@ export class UserResolver {
|
|||||||
/* uncomment this, when you need the activation link on the console */
|
/* uncomment this, when you need the activation link on the console */
|
||||||
// In case EMails are disabled log the activation link for the user
|
// In case EMails are disabled log the activation link for the user
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
logger.debug(`Reset password link: ${activationLink(optInCode)}`)
|
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
|
||||||
}
|
}
|
||||||
logger.info(`forgotPassword(${email}) successful...`)
|
logger.info(`forgotPassword(${email}) successful...`)
|
||||||
|
|
||||||
@ -548,19 +621,29 @@ export class UserResolver {
|
|||||||
logger.info(`setPassword(${code}, ***)...`)
|
logger.info(`setPassword(${code}, ***)...`)
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isPassword(password)) {
|
if (!isPassword(password)) {
|
||||||
|
logger.error('Password entered is lexically invalid')
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load code
|
// Load code
|
||||||
|
/*
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||||
logger.error('Could not login with emailVerificationCode')
|
logger.error('Could not login with emailVerificationCode')
|
||||||
throw new 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
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInValid(optInCode)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
@ -568,14 +651,11 @@ export class UserResolver {
|
|||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug('optInCode is valid...')
|
logger.debug('EmailVerificationCode is valid...')
|
||||||
|
|
||||||
// load user
|
// load user
|
||||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
const user = userContact.user
|
||||||
logger.error('Could not find corresponding Login User')
|
logger.debug('user with EmailVerificationCode found...')
|
||||||
throw new Error('Could not find corresponding Login User')
|
|
||||||
})
|
|
||||||
logger.debug('user with optInCode found...')
|
|
||||||
|
|
||||||
// Generate Passphrase if needed
|
// Generate Passphrase if needed
|
||||||
if (!user.passphrase) {
|
if (!user.passphrase) {
|
||||||
@ -595,10 +675,10 @@ export class UserResolver {
|
|||||||
logger.debug('Passphrase is valid...')
|
logger.debug('Passphrase is valid...')
|
||||||
|
|
||||||
// Activate EMail
|
// Activate EMail
|
||||||
user.emailChecked = true
|
userContact.emailChecked = true
|
||||||
|
|
||||||
// Update Password
|
// 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 keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||||
@ -608,7 +688,9 @@ export class UserResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
|
||||||
|
const event = new Event()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Save user
|
// Save user
|
||||||
@ -616,12 +698,21 @@ export class UserResolver {
|
|||||||
logger.error('error saving user: ' + error)
|
logger.error('error saving user: ' + error)
|
||||||
throw new 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()
|
await queryRunner.commitTransaction()
|
||||||
logger.info('User data written successfully...')
|
logger.info('User and UserContact data written successfully...')
|
||||||
|
|
||||||
|
const eventActivateAccount = new EventActivateAccount()
|
||||||
|
eventActivateAccount.userId = user.id
|
||||||
|
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error('Error on writing User data:' + e)
|
logger.error('Error on writing User and UserContact data:' + e)
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
@ -629,11 +720,11 @@ export class UserResolver {
|
|||||||
|
|
||||||
// Sign into Klicktipp
|
// Sign into Klicktipp
|
||||||
// TODO do we always signUp the user? How to handle things with old users?
|
// 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 {
|
try {
|
||||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error subscribe to klicktipp:' + e)
|
logger.error('Error subscribe to klicktipp:' + e)
|
||||||
@ -652,10 +743,10 @@ export class UserResolver {
|
|||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||||
logger.info(`queryOptIn(${optIn})...`)
|
logger.info(`queryOptIn(${optIn})...`)
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||||
logger.debug(`found optInCode=${optInCode}`)
|
logger.debug(`found optInCode=${userContact}`)
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isOptInValid(optInCode)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
)
|
||||||
@ -703,7 +794,10 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
// 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()) {
|
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||||
logger.error(`Old password is invalid`)
|
logger.error(`Old password is invalid`)
|
||||||
throw new Error(`Old password is invalid`)
|
throw new Error(`Old password is invalid`)
|
||||||
@ -711,7 +805,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||||
logger.debug('oldPassword decrypted...')
|
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...')
|
logger.debug('newPasswordHash created...')
|
||||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||||
logger.debug('PrivateKey encrypted...')
|
logger.debug('PrivateKey encrypted...')
|
||||||
@ -723,10 +820,11 @@ export class UserResolver {
|
|||||||
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||||
|
logger.error('error saving user: ' + error)
|
||||||
throw new Error('error saving user: ' + error)
|
throw new Error('error saving user: ' + error)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -747,12 +845,8 @@ export class UserResolver {
|
|||||||
@Query(() => Boolean)
|
@Query(() => Boolean)
|
||||||
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
||||||
logger.info(`hasElopage()...`)
|
logger.info(`hasElopage()...`)
|
||||||
const userEntity = context.user
|
const userEntity = getUser(context)
|
||||||
if (!userEntity) {
|
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
|
||||||
logger.info('missing context.user for EloPage-check')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const elopageBuys = hasElopageBuys(userEntity.email)
|
|
||||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||||
return elopageBuys
|
return elopageBuys
|
||||||
}
|
}
|
||||||
@ -788,19 +882,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 isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
||||||
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
||||||
// time is given in minutes
|
// time is given in minutes
|
||||||
return timeElapsed <= duration * 60 * 1000
|
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 => {
|
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
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 => {
|
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||||
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
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 } => {
|
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||||
if (time > 60) {
|
if (time > 60) {
|
||||||
|
|||||||
@ -15,14 +15,21 @@ export const validateContribution = (
|
|||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
creationDate: Date,
|
creationDate: Date,
|
||||||
): void => {
|
): void => {
|
||||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
logger.trace('isContributionValid: ', creations, amount, creationDate)
|
||||||
const index = getCreationIndex(creationDate.getMonth())
|
const index = getCreationIndex(creationDate.getMonth())
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
logger.error(
|
||||||
|
'No information for available creations with the given creationDate=',
|
||||||
|
creationDate,
|
||||||
|
)
|
||||||
throw new Error('No information for available creations for the given date')
|
throw new Error('No information for available creations for the given date')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount.greaterThan(creations[index].toString())) {
|
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(
|
throw new Error(
|
||||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||||
)
|
)
|
||||||
@ -41,7 +48,7 @@ export const getUserCreations = async (
|
|||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
|
|
||||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
logger.trace('getUserCreations dateFilter=', dateFilter)
|
||||||
|
|
||||||
const unionString = includePending
|
const unionString = includePending
|
||||||
? `
|
? `
|
||||||
@ -51,6 +58,7 @@ export const getUserCreations = async (
|
|||||||
AND contribution_date >= ${dateFilter}
|
AND contribution_date >= ${dateFilter}
|
||||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||||
: ''
|
: ''
|
||||||
|
logger.trace('getUserCreations unionString=', unionString)
|
||||||
|
|
||||||
const unionQuery = await queryRunner.manager.query(`
|
const unionQuery = await queryRunner.manager.query(`
|
||||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||||
@ -62,6 +70,7 @@ export const getUserCreations = async (
|
|||||||
GROUP BY month, userId
|
GROUP BY month, userId
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
`)
|
`)
|
||||||
|
logger.trace('getUserCreations unionQuery=', unionQuery)
|
||||||
|
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
|
|
||||||
@ -82,6 +91,7 @@ export const getUserCreations = async (
|
|||||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||||
logger.trace('getUserCreation', id, includePending)
|
logger.trace('getUserCreation', id, includePending)
|
||||||
const creations = await getUserCreations([id], includePending)
|
const creations = await getUserCreations([id], includePending)
|
||||||
|
logger.trace('getUserCreation creations=', creations)
|
||||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
40
backend/src/mailer/sendAddedContributionMessageEmail.test.ts
Normal file
40
backend/src/mailer/sendAddedContributionMessageEmail.test.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { sendAddedContributionMessageEmail } from './sendAddedContributionMessageEmail'
|
||||||
|
import { sendEMail } from './sendEMail'
|
||||||
|
|
||||||
|
jest.mock('./sendEMail', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendEMail: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendAddedContributionMessageEmail', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await sendAddedContributionMessageEmail({
|
||||||
|
senderFirstName: 'Peter',
|
||||||
|
senderLastName: 'Lustig',
|
||||||
|
recipientFirstName: 'Bibi',
|
||||||
|
recipientLastName: 'Bloxberg',
|
||||||
|
recipientEmail: 'bibi@bloxberg.de',
|
||||||
|
senderEmail: 'peter@lustig.de',
|
||||||
|
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||||
|
message: 'Was für ein Besen ist es geworden?',
|
||||||
|
overviewURL: 'http://localhost/overview',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls sendEMail', () => {
|
||||||
|
expect(sendEMail).toBeCalledWith({
|
||||||
|
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||||
|
subject: 'Gradido Frage zur Schöpfung',
|
||||||
|
text:
|
||||||
|
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||||
|
expect.stringContaining('Peter Lustig') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Du hast soeben zu deinem eingereichten Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" eine Rückfrage von Peter Lustig erhalten.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('Was für ein Besen ist es geworden?') &&
|
||||||
|
expect.stringContaining('http://localhost/overview'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
26
backend/src/mailer/sendAddedContributionMessageEmail.ts
Normal file
26
backend/src/mailer/sendAddedContributionMessageEmail.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import { sendEMail } from './sendEMail'
|
||||||
|
import { contributionMessageReceived } from './text/contributionMessageReceived'
|
||||||
|
|
||||||
|
export const sendAddedContributionMessageEmail = (data: {
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
recipientFirstName: string
|
||||||
|
recipientLastName: string
|
||||||
|
recipientEmail: string
|
||||||
|
senderEmail: string
|
||||||
|
contributionMemo: string
|
||||||
|
message: string
|
||||||
|
overviewURL: string
|
||||||
|
}): Promise<boolean> => {
|
||||||
|
logger.info(
|
||||||
|
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
|
||||||
|
subject=${contributionMessageReceived.de.subject},
|
||||||
|
text=${contributionMessageReceived.de.text(data)}`,
|
||||||
|
)
|
||||||
|
return sendEMail({
|
||||||
|
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
|
||||||
|
subject: contributionMessageReceived.de.subject,
|
||||||
|
text: contributionMessageReceived.de.text(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
39
backend/src/mailer/sendContributionConfirmedEmail.test.ts
Normal file
39
backend/src/mailer/sendContributionConfirmedEmail.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { sendContributionConfirmedEmail } from './sendContributionConfirmedEmail'
|
||||||
|
import { sendEMail } from './sendEMail'
|
||||||
|
|
||||||
|
jest.mock('./sendEMail', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
sendEMail: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sendContributionConfirmedEmail', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await sendContributionConfirmedEmail({
|
||||||
|
senderFirstName: 'Peter',
|
||||||
|
senderLastName: 'Lustig',
|
||||||
|
recipientFirstName: 'Bibi',
|
||||||
|
recipientLastName: 'Bloxberg',
|
||||||
|
recipientEmail: 'bibi@bloxberg.de',
|
||||||
|
contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||||
|
contributionAmount: new Decimal(200.0),
|
||||||
|
overviewURL: 'http://localhost/overview',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls sendEMail', () => {
|
||||||
|
expect(sendEMail).toBeCalledWith({
|
||||||
|
to: 'Bibi Bloxberg <bibi@bloxberg.de>',
|
||||||
|
subject: 'Schöpfung wurde bestätigt',
|
||||||
|
text:
|
||||||
|
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||||
|
expect.stringContaining(
|
||||||
|
'Dein Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben bestätigt.',
|
||||||
|
) &&
|
||||||
|
expect.stringContaining('Betrag: 200,00 GDD') &&
|
||||||
|
expect.stringContaining('Link zu deinem Konto: http://localhost/overview'),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
26
backend/src/mailer/sendContributionConfirmedEmail.ts
Normal file
26
backend/src/mailer/sendContributionConfirmedEmail.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
import { sendEMail } from './sendEMail'
|
||||||
|
import { contributionConfirmed } from './text/contributionConfirmed'
|
||||||
|
|
||||||
|
export const sendContributionConfirmedEmail = (data: {
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
recipientFirstName: string
|
||||||
|
recipientLastName: string
|
||||||
|
recipientEmail: string
|
||||||
|
contributionMemo: string
|
||||||
|
contributionAmount: Decimal
|
||||||
|
overviewURL: string
|
||||||
|
}): Promise<boolean> => {
|
||||||
|
logger.info(
|
||||||
|
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>,
|
||||||
|
subject=${contributionConfirmed.de.subject},
|
||||||
|
text=${contributionConfirmed.de.text(data)}`,
|
||||||
|
)
|
||||||
|
return sendEMail({
|
||||||
|
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`,
|
||||||
|
subject: contributionConfirmed.de.subject,
|
||||||
|
text: contributionConfirmed.de.text(data),
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -73,7 +73,7 @@ describe('sendEMail', () => {
|
|||||||
it('calls sendMail of transporter', () => {
|
it('calls sendMail of transporter', () => {
|
||||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||||
to: 'receiver@mail.org',
|
to: `${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||||
cc: 'support@gradido.net',
|
cc: 'support@gradido.net',
|
||||||
subject: 'Subject',
|
subject: 'Subject',
|
||||||
text: 'Text text text',
|
text: 'Text text text',
|
||||||
|
|||||||
@ -19,6 +19,12 @@ export const sendEMail = async (emailDef: {
|
|||||||
logger.info(`Emails are disabled via config...`)
|
logger.info(`Emails are disabled via config...`)
|
||||||
return false
|
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({
|
const transporter = createTransport({
|
||||||
host: CONFIG.EMAIL_SMTP_URL,
|
host: CONFIG.EMAIL_SMTP_URL,
|
||||||
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
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',
|
email: 'peter@lustig.de',
|
||||||
senderEmail: 'bibi@bloxberg.de',
|
senderEmail: 'bibi@bloxberg.de',
|
||||||
amount: new Decimal(42.0),
|
amount: new Decimal(42.0),
|
||||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
|
||||||
overviewURL: 'http://localhost/overview',
|
overviewURL: 'http://localhost/overview',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -33,7 +32,6 @@ describe('sendTransactionReceivedEmail', () => {
|
|||||||
expect.stringContaining('42,00 GDD') &&
|
expect.stringContaining('42,00 GDD') &&
|
||||||
expect.stringContaining('Bibi Bloxberg') &&
|
expect.stringContaining('Bibi Bloxberg') &&
|
||||||
expect.stringContaining('(bibi@bloxberg.de)') &&
|
expect.stringContaining('(bibi@bloxberg.de)') &&
|
||||||
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') &&
|
|
||||||
expect.stringContaining('http://localhost/overview'),
|
expect.stringContaining('http://localhost/overview'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export const sendTransactionReceivedEmail = (data: {
|
|||||||
email: string
|
email: string
|
||||||
senderEmail: string
|
senderEmail: string
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
memo: string
|
|
||||||
overviewURL: string
|
overviewURL: string
|
||||||
}): Promise<boolean> => {
|
}): Promise<boolean> => {
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
31
backend/src/mailer/text/contributionConfirmed.ts
Normal file
31
backend/src/mailer/text/contributionConfirmed.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import Decimal from 'decimal.js-light'
|
||||||
|
|
||||||
|
export const contributionConfirmed = {
|
||||||
|
de: {
|
||||||
|
subject: 'Schöpfung wurde bestätigt',
|
||||||
|
text: (data: {
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
recipientFirstName: string
|
||||||
|
recipientLastName: string
|
||||||
|
contributionMemo: string
|
||||||
|
contributionAmount: Decimal
|
||||||
|
overviewURL: string
|
||||||
|
}): string =>
|
||||||
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||||
|
|
||||||
|
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!
|
||||||
|
|
||||||
|
Mit freundlichen Grüßen,
|
||||||
|
dein Gradido-Team
|
||||||
|
|
||||||
|
|
||||||
|
Link zu deinem Konto: ${data.overviewURL}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
28
backend/src/mailer/text/contributionMessageReceived.ts
Normal file
28
backend/src/mailer/text/contributionMessageReceived.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export const contributionMessageReceived = {
|
||||||
|
de: {
|
||||||
|
subject: 'Gradido Frage zur Schöpfung',
|
||||||
|
text: (data: {
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
recipientFirstName: string
|
||||||
|
recipientLastName: string
|
||||||
|
recipientEmail: string
|
||||||
|
senderEmail: string
|
||||||
|
contributionMemo: string
|
||||||
|
message: string
|
||||||
|
overviewURL: string
|
||||||
|
}): string =>
|
||||||
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
|
||||||
|
|
||||||
|
du hast soeben zu deinem eingereichten Gemeinwohl-Beitrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
|
||||||
|
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
}
|
||||||
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
|
email: string
|
||||||
senderEmail: string
|
senderEmail: string
|
||||||
amount: Decimal
|
amount: Decimal
|
||||||
memo: string
|
|
||||||
overviewURL: string
|
overviewURL: string
|
||||||
}): string =>
|
}): string =>
|
||||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||||
@ -19,16 +18,12 @@ export const transactionReceived = {
|
|||||||
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
||||||
data.senderLastName
|
data.senderLastName
|
||||||
} (${data.senderEmail}) erhalten.
|
} (${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!
|
Bitte antworte nicht auf diese E-Mail!
|
||||||
|
|
||||||
Mit freundlichen Grüßen,
|
Mit freundlichen Grüßen,
|
||||||
dein Gradido-Team
|
dein Gradido-Team`,
|
||||||
|
|
||||||
|
|
||||||
Link zu deinem Konto: ${data.overviewURL}`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,11 @@ export const contributionLinkFactory = async (
|
|||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
|
||||||
// login as admin
|
// 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 query({
|
||||||
|
query: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
|
||||||
const variables = {
|
const variables = {
|
||||||
amount: contributionLink.amount,
|
amount: contributionLink.amount,
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||||
import { login } from '@/seeds/graphql/queries'
|
import { login } from '@/seeds/graphql/queries'
|
||||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
import { User } from '@entity/User'
|
|
||||||
import { Transaction } from '@entity/Transaction'
|
import { Transaction } from '@entity/Transaction'
|
||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution } from '@entity/Contribution'
|
||||||
|
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
|
||||||
// import CONFIG from '@/config/index'
|
// import CONFIG from '@/config/index'
|
||||||
|
|
||||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||||
@ -19,29 +20,41 @@ export const creationFactory = async (
|
|||||||
creation: CreationInterface,
|
creation: CreationInterface,
|
||||||
): Promise<Contribution | void> => {
|
): Promise<Contribution | void> => {
|
||||||
const { mutate, query } = client
|
const { mutate, query } = client
|
||||||
|
logger.trace('creationFactory...')
|
||||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
await query({ query: 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
|
// TODO it would be nice to have this mutation return the id
|
||||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
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({
|
const pendingCreation = await Contribution.findOneOrFail({
|
||||||
where: { userId: user.id, amount: creation.amount },
|
where: { userId: user.id, amount: creation.amount },
|
||||||
order: { createdAt: 'DESC' },
|
order: { createdAt: 'DESC' },
|
||||||
})
|
})
|
||||||
|
logger.trace(
|
||||||
|
'creationFactory... after Contribution.findOneOrFail pendingCreation=',
|
||||||
|
pendingCreation,
|
||||||
|
)
|
||||||
if (creation.confirmed) {
|
if (creation.confirmed) {
|
||||||
|
logger.trace('creationFactory... creation.confirmed=', creation.confirmed)
|
||||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||||
|
logger.trace('creationFactory... after confirmContribution')
|
||||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||||
|
logger.trace(
|
||||||
|
'creationFactory... after Contribution.findOneOrFail confirmedCreation=',
|
||||||
|
confirmedCreation,
|
||||||
|
)
|
||||||
|
|
||||||
if (creation.moveCreationDate) {
|
if (creation.moveCreationDate) {
|
||||||
|
logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate)
|
||||||
const transaction = await Transaction.findOneOrFail({
|
const transaction = await Transaction.findOneOrFail({
|
||||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||||
order: { balanceDate: 'DESC' },
|
order: { balanceDate: 'DESC' },
|
||||||
})
|
})
|
||||||
|
logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction)
|
||||||
|
|
||||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||||
confirmedCreation.contributionDate = new Date(
|
confirmedCreation.contributionDate = new Date(
|
||||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||||
@ -52,11 +65,17 @@ export const creationFactory = async (
|
|||||||
transaction.balanceDate = new Date(
|
transaction.balanceDate = new Date(
|
||||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||||
)
|
)
|
||||||
|
logger.trace('creationFactory... before transaction.save transaction=', transaction)
|
||||||
await transaction.save()
|
await transaction.save()
|
||||||
|
logger.trace(
|
||||||
|
'creationFactory... before confirmedCreation.save confirmedCreation=',
|
||||||
|
confirmedCreation,
|
||||||
|
)
|
||||||
await confirmedCreation.save()
|
await confirmedCreation.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
logger.trace('creationFactory... pendingCreation=', pendingCreation)
|
||||||
return pendingCreation
|
return pendingCreation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
|
||||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
@ -15,17 +14,23 @@ export const userFactory = async (
|
|||||||
createUser: { id },
|
createUser: { id },
|
||||||
},
|
},
|
||||||
} = await mutate({ mutation: createUser, variables: user })
|
} = 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) {
|
if (user.emailChecked) {
|
||||||
const optin = await LoginEmailOptIn.findOneOrFail({ userId: id })
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: setPassword,
|
mutation: setPassword,
|
||||||
variables: { password: 'Aa12345_', code: optin.verificationCode },
|
variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// get user from database
|
// get last changes of user from database
|
||||||
const dbUser = await User.findOneOrFail({ id })
|
dbUser = await User.findOneOrFail({ id })
|
||||||
|
|
||||||
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
||||||
if (user.createdAt) dbUser.createdAt = user.createdAt
|
if (user.createdAt) dbUser.createdAt = user.createdAt
|
||||||
@ -34,5 +39,8 @@ export const userFactory = async (
|
|||||||
await dbUser.save()
|
await dbUser.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get last changes of user from database
|
||||||
|
// dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
|
||||||
|
|
||||||
return dbUser
|
return dbUser
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,3 +261,31 @@ export const deleteContribution = gql`
|
|||||||
deleteContribution(id: $id)
|
deleteContribution(id: $id)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const createContributionMessage = gql`
|
||||||
|
mutation ($contributionId: Float!, $message: String!) {
|
||||||
|
createContributionMessage(contributionId: $contributionId, message: $message) {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
type
|
||||||
|
userFirstName
|
||||||
|
userLastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const adminCreateContributionMessage = gql`
|
||||||
|
mutation ($contributionId: Float!, $message: String!) {
|
||||||
|
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
type
|
||||||
|
userFirstName
|
||||||
|
userLastName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -292,3 +292,26 @@ export const searchAdminUsers = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const listContributionMessages = gql`
|
||||||
|
query ($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
|
||||||
|
listContributionMessages(
|
||||||
|
contributionId: $contributionId
|
||||||
|
pageSize: $pageSize
|
||||||
|
currentPage: $currentPage
|
||||||
|
order: $order
|
||||||
|
) {
|
||||||
|
count
|
||||||
|
messages {
|
||||||
|
id
|
||||||
|
message
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
type
|
||||||
|
userFirstName
|
||||||
|
userLastName
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import createServer from '../server/createServer'
|
import createServer from '../server/createServer'
|
||||||
import { createTestClient } from 'apollo-server-testing'
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
|
||||||
@ -50,11 +51,14 @@ const run = async () => {
|
|||||||
const seedClient = createTestClient(server.apollo)
|
const seedClient = createTestClient(server.apollo)
|
||||||
const { con } = server
|
const { con } = server
|
||||||
await cleanDB()
|
await cleanDB()
|
||||||
|
logger.info('##seed## clean database successful...')
|
||||||
|
|
||||||
// seed the standard users
|
// seed the standard users
|
||||||
for (let i = 0; i < users.length; i++) {
|
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
|
// seed 100 random users
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
@ -64,7 +68,9 @@ const run = async () => {
|
|||||||
email: internet.email(),
|
email: internet.email(),
|
||||||
language: datatype.boolean() ? 'en' : 'de',
|
language: datatype.boolean() ? 'en' : 'de',
|
||||||
})
|
})
|
||||||
|
logger.info(`##seed## seed ${i}. random user`)
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all random users successful...')
|
||||||
|
|
||||||
// create GDD
|
// create GDD
|
||||||
for (let i = 0; i < creations.length; i++) {
|
for (let i = 0; i < creations.length; i++) {
|
||||||
@ -73,16 +79,19 @@ const run = async () => {
|
|||||||
// eslint-disable-next-line no-empty
|
// 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)
|
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
|
// create Transaction Links
|
||||||
for (let i = 0; i < transactionLinks.length; i++) {
|
for (let i = 0; i < transactionLinks.length; i++) {
|
||||||
await transactionLinkFactory(seedClient, transactionLinks[i])
|
await transactionLinkFactory(seedClient, transactionLinks[i])
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all transactionLinks successful...')
|
||||||
|
|
||||||
// create Contribution Links
|
// create Contribution Links
|
||||||
for (let i = 0; i < contributionLinks.length; i++) {
|
for (let i = 0; i < contributionLinks.length; i++) {
|
||||||
await contributionLinkFactory(seedClient, contributionLinks[i])
|
await contributionLinkFactory(seedClient, contributionLinks[i])
|
||||||
}
|
}
|
||||||
|
logger.info('##seed## seeding all contributionLinks successful...')
|
||||||
|
|
||||||
await con.close()
|
await con.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export interface Context {
|
|||||||
setHeaders: { key: string; value: string }[]
|
setHeaders: { key: string; value: string }[]
|
||||||
role?: Role
|
role?: Role
|
||||||
user?: dbUser
|
user?: dbUser
|
||||||
|
clientRequestTime?: string
|
||||||
// hack to use less DB calls for Balance Resolver
|
// hack to use less DB calls for Balance Resolver
|
||||||
lastTransaction?: dbTransaction
|
lastTransaction?: dbTransaction
|
||||||
transactionCount?: number
|
transactionCount?: number
|
||||||
@ -18,14 +19,17 @@ export interface Context {
|
|||||||
|
|
||||||
const context = (args: ExpressContext): Context => {
|
const context = (args: ExpressContext): Context => {
|
||||||
const authorization = args.req.headers.authorization
|
const authorization = args.req.headers.authorization
|
||||||
let token: string | null = null
|
const clientRequestTime = args.req.headers.clientrequesttime
|
||||||
if (authorization) {
|
const context: Context = {
|
||||||
token = authorization.replace(/^Bearer /, '')
|
token: null,
|
||||||
}
|
|
||||||
const context = {
|
|
||||||
token,
|
|
||||||
setHeaders: [],
|
setHeaders: [],
|
||||||
}
|
}
|
||||||
|
if (authorization) {
|
||||||
|
context.token = authorization.replace(/^Bearer /, '')
|
||||||
|
}
|
||||||
|
if (clientRequestTime && typeof clientRequestTime === 'string') {
|
||||||
|
context.clientRequestTime = clientRequestTime
|
||||||
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -75,6 +75,9 @@ const createServer = async (
|
|||||||
logger,
|
logger,
|
||||||
})
|
})
|
||||||
apollo.applyMiddleware({ app, path: '/' })
|
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')
|
logger.debug('createServer...successful')
|
||||||
return { apollo, app, con }
|
return { apollo, app, con }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,28 +1,39 @@
|
|||||||
import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm'
|
import SearchUsersFilters from '@/graphql/arg/SearchUsersFilters'
|
||||||
import { User } from '@entity/User'
|
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
|
||||||
|
import { User as DbUser } from '@entity/User'
|
||||||
|
|
||||||
@EntityRepository(User)
|
@EntityRepository(DbUser)
|
||||||
export class UserRepository extends Repository<User> {
|
export class UserRepository extends Repository<DbUser> {
|
||||||
async findByPubkeyHex(pubkeyHex: string): Promise<User> {
|
async findByPubkeyHex(pubkeyHex: string): Promise<DbUser> {
|
||||||
return this.createQueryBuilder('user')
|
const dbUser = await this.createQueryBuilder('user')
|
||||||
|
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||||
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
|
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
|
||||||
.getOneOrFail()
|
.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(
|
async findBySearchCriteriaPagedFiltered(
|
||||||
select: string[],
|
select: string[],
|
||||||
searchCriteria: string,
|
searchCriteria: string,
|
||||||
filterCriteria: ObjectLiteral[],
|
filters: SearchUsersFilters,
|
||||||
currentPage: number,
|
currentPage: number,
|
||||||
pageSize: number,
|
pageSize: number,
|
||||||
): Promise<[User[], number]> {
|
): Promise<[DbUser[], number]> {
|
||||||
const query = await this.createQueryBuilder('user')
|
const query = this.createQueryBuilder('user')
|
||||||
.select(select)
|
.select(select)
|
||||||
|
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||||
.withDeleted()
|
.withDeleted()
|
||||||
.where(
|
.where(
|
||||||
new Brackets((qb) => {
|
new Brackets((qb) => {
|
||||||
qb.where(
|
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}%`,
|
name: `%${searchCriteria}%`,
|
||||||
lastName: `%${searchCriteria}%`,
|
lastName: `%${searchCriteria}%`,
|
||||||
@ -31,9 +42,23 @@ export class UserRepository extends Repository<User> {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
/*
|
||||||
filterCriteria.forEach((filter) => {
|
filterCriteria.forEach((filter) => {
|
||||||
query.andWhere(filter)
|
query.andWhere(filter)
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
|
if (filters) {
|
||||||
|
if (filters.byActivated !== null) {
|
||||||
|
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
|
||||||
|
// filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.byDeleted !== null) {
|
||||||
|
// filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||||
|
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
.take(pageSize)
|
.take(pageSize)
|
||||||
.skip((currentPage - 1) * pageSize)
|
.skip((currentPage - 1) * pageSize)
|
||||||
|
|||||||
@ -2,22 +2,26 @@
|
|||||||
|
|
||||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||||
import { User as dbUser } from '@entity/User'
|
import { User as dbUser } from '@entity/User'
|
||||||
|
import { UserContact } from '@entity/UserContact'
|
||||||
|
// import { UserContact as EmailContact } from '@entity/UserContact'
|
||||||
import { User } from '@model/User'
|
import { User } from '@model/User'
|
||||||
|
|
||||||
const communityDbUser: dbUser = {
|
const communityDbUser: dbUser = {
|
||||||
id: -1,
|
id: -1,
|
||||||
gradidoID: '11111111-2222-4333-4444-55555555',
|
gradidoID: '11111111-2222-4333-4444-55555555',
|
||||||
alias: '',
|
alias: '',
|
||||||
email: 'support@gradido.net',
|
// email: 'support@gradido.net',
|
||||||
|
emailContact: new UserContact(),
|
||||||
|
emailId: -1,
|
||||||
firstName: 'Gradido',
|
firstName: 'Gradido',
|
||||||
lastName: 'Akademie',
|
lastName: 'Akademie',
|
||||||
pubKey: Buffer.from(''),
|
pubKey: Buffer.from(''),
|
||||||
privKey: Buffer.from(''),
|
privKey: Buffer.from(''),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
password: BigInt(0),
|
password: BigInt(0),
|
||||||
emailHash: Buffer.from(''),
|
// emailHash: Buffer.from(''),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
emailChecked: false,
|
// emailChecked: false,
|
||||||
language: '',
|
language: '',
|
||||||
isAdmin: null,
|
isAdmin: null,
|
||||||
publisherId: 0,
|
publisherId: 0,
|
||||||
|
|||||||
28
backend/src/util/klicktipp.ts
Normal file
28
backend/src/util/klicktipp.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import connection from '@/typeorm/connection'
|
||||||
|
import { getKlickTippUser } from '@/apis/KlicktippController'
|
||||||
|
import { User } from '@entity/User'
|
||||||
|
|
||||||
|
export async function retrieveNotRegisteredEmails(): Promise<string[]> {
|
||||||
|
const con = await connection()
|
||||||
|
if (!con) {
|
||||||
|
throw new Error('No connection to database')
|
||||||
|
}
|
||||||
|
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.emailContact.email)
|
||||||
|
} catch (err) {
|
||||||
|
notRegisteredUser.push(user.emailContact.email)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(`${user.emailContact.email}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await con.close()
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('User die nicht bei KlickTipp vorhanden sind: ', notRegisteredUser)
|
||||||
|
return notRegisteredUser
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieveNotRegisteredEmails()
|
||||||
@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
import { LoginElopageBuys } from '@entity/LoginElopageBuys'
|
||||||
import { UserResolver } from '@/graphql/resolver/UserResolver'
|
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> => {
|
export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||||
// eslint-disable-next-line no-console
|
// 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?
|
// 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
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Did not create User - already exists with email: ${email}`)
|
console.log(`Did not create User - already exists with email: ${email}`)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm'
|
} from 'typeorm'
|
||||||
import { Contribution } from '../Contribution'
|
import { Contribution } from '../Contribution'
|
||||||
|
import { User } from '../User'
|
||||||
|
|
||||||
@Entity('contribution_messages', {
|
@Entity('contribution_messages', {
|
||||||
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||||
@ -26,6 +27,10 @@ export class ContributionMessage extends BaseEntity {
|
|||||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||||
userId: number
|
userId: number
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.messages)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User
|
||||||
|
|
||||||
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
message: string
|
message: string
|
||||||
|
|
||||||
|
|||||||
116
database/entity/0047-messages_tables/User.ts
Normal file
116
database/entity/0047-messages_tables/User.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
OneToMany,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { Contribution } from '../Contribution'
|
||||||
|
import { ContributionMessage } from '../ContributionMessage'
|
||||||
|
|
||||||
|
@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,
|
||||||
|
unique: true,
|
||||||
|
collation: 'utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
gradidoID: string
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
name: 'alias',
|
||||||
|
length: 20,
|
||||||
|
nullable: true,
|
||||||
|
unique: 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
|
||||||
|
|
||||||
|
@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: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
|
||||||
|
emailHash: Buffer
|
||||||
|
|
||||||
|
@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[]
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
DeleteDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
} from 'typeorm'
|
||||||
|
import { Contribution } from '../Contribution'
|
||||||
|
import { User } from '../User'
|
||||||
|
|
||||||
|
@Entity('contribution_messages', {
|
||||||
|
engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci',
|
||||||
|
})
|
||||||
|
export class ContributionMessage extends BaseEntity {
|
||||||
|
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ name: 'contribution_id', unsigned: true, nullable: false })
|
||||||
|
contributionId: number
|
||||||
|
|
||||||
|
@ManyToOne(() => Contribution, (contribution) => contribution.messages)
|
||||||
|
@JoinColumn({ name: 'contribution_id' })
|
||||||
|
contribution: Contribution
|
||||||
|
|
||||||
|
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@ManyToOne(() => User, (user) => user.messages)
|
||||||
|
@JoinColumn({ name: 'user_id' })
|
||||||
|
user: User
|
||||||
|
|
||||||
|
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
message: string
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@Column({ type: 'datetime', default: null, nullable: true, name: 'updated_at' })
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
@DeleteDateColumn({ name: 'deleted_at' })
|
||||||
|
deletedAt: Date | null
|
||||||
|
|
||||||
|
@Column({ name: 'deleted_by', default: null, unsigned: true, nullable: true })
|
||||||
|
deletedBy: number
|
||||||
|
|
||||||
|
@Column({ length: 12, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||||
|
type: string
|
||||||
|
|
||||||
|
@Column({ name: 'is_moderator', type: 'bool', nullable: false, default: false })
|
||||||
|
isModerator: boolean
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
export { ContributionMessage } from './0047-messages_tables/ContributionMessage'
|
export { ContributionMessage } from './0048-add_is_moderator_to_contribution_messages/ContributionMessage'
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { User } from './0046-adapt_users_table_for_gradidoid/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 { Transaction } from './Transaction'
|
||||||
import { TransactionLink } from './TransactionLink'
|
import { TransactionLink } from './TransactionLink'
|
||||||
import { User } from './User'
|
import { User } from './User'
|
||||||
|
import { UserContact } from './UserContact'
|
||||||
import { Contribution } from './Contribution'
|
import { Contribution } from './Contribution'
|
||||||
import { EventProtocol } from './EventProtocol'
|
import { EventProtocol } from './EventProtocol'
|
||||||
import { ContributionMessage } from './ContributionMessage'
|
import { ContributionMessage } from './ContributionMessage'
|
||||||
@ -20,4 +21,5 @@ export const entities = [
|
|||||||
User,
|
User,
|
||||||
EventProtocol,
|
EventProtocol,
|
||||||
ContributionMessage,
|
ContributionMessage,
|
||||||
|
UserContact,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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 \`contribution_messages\` ADD COLUMN \`is_moderator\` boolean NOT NULL DEFAULT false;`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||||
|
await queryFn(`ALTER TABLE \`contribution_messages\` DROP COLUMN \`is_moderator\`;`)
|
||||||
|
}
|
||||||
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;')
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-database",
|
"name": "gradido-database",
|
||||||
"version": "1.11.0",
|
"version": "1.12.1",
|
||||||
"description": "Gradido Database Tool to execute database migrations",
|
"description": "Gradido Database Tool to execute database migrations",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/database",
|
"repository": "https://github.com/gradido/gradido/database",
|
||||||
|
|||||||
@ -26,10 +26,11 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
|||||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||||
|
|
||||||
# backend
|
# backend
|
||||||
BACKEND_CONFIG_VERSION=v9.2022-07-07
|
BACKEND_CONFIG_VERSION=v10.2022-09-20
|
||||||
|
|
||||||
JWT_EXPIRES_IN=10m
|
JWT_EXPIRES_IN=10m
|
||||||
GDT_API_URL=https://gdt.gradido.net
|
GDT_API_URL=https://gdt.gradido.net
|
||||||
|
ENV_NAME=stage1
|
||||||
|
|
||||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||||
|
|
||||||
@ -40,6 +41,8 @@ KLICKTIPP_APIKEY_DE=
|
|||||||
KLICKTIPP_APIKEY_EN=
|
KLICKTIPP_APIKEY_EN=
|
||||||
|
|
||||||
EMAIL=true
|
EMAIL=true
|
||||||
|
EMAIL_TEST_MODUS=false
|
||||||
|
EMAIL_TEST_RECEIVER=test_team@gradido.net
|
||||||
EMAIL_USERNAME=peter@lustig.de
|
EMAIL_USERNAME=peter@lustig.de
|
||||||
EMAIL_SENDER=peter@lustig.de
|
EMAIL_SENDER=peter@lustig.de
|
||||||
EMAIL_PASSWORD=1234
|
EMAIL_PASSWORD=1234
|
||||||
@ -61,7 +64,7 @@ EVENT_PROTOCOL_DISABLED=false
|
|||||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||||
|
|
||||||
# frontend
|
# frontend
|
||||||
FRONTEND_CONFIG_VERSION=v2.2022-04-07
|
FRONTEND_CONFIG_VERSION=v3.2022-09-16
|
||||||
|
|
||||||
GRAPHQL_URI=https://stage1.gradido.net/graphql
|
GRAPHQL_URI=https://stage1.gradido.net/graphql
|
||||||
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
|
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
|
||||||
@ -77,6 +80,8 @@ META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natü
|
|||||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||||
|
|
||||||
|
SUPPORT_MAIL=support@supportmail.com
|
||||||
|
|
||||||
# admin
|
# admin
|
||||||
ADMIN_CONFIG_VERSION=v1.2022-03-18
|
ADMIN_CONFIG_VERSION=v1.2022-03-18
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,12 @@
|
|||||||
# How to do this is described in detail in [setup.md](./setup.md)
|
# How to do this is described in detail in [setup.md](./setup.md)
|
||||||
|
|
||||||
# Find current directory & configure paths
|
# Find current directory & configure paths
|
||||||
|
## For manualy use in terminal
|
||||||
|
## set -o allexport
|
||||||
|
## SCRIPT_DIR=$(pwd)
|
||||||
|
## PROJECT_ROOT=$SCRIPT_DIR/../..
|
||||||
|
## set +o allexport
|
||||||
|
# Use here in script
|
||||||
set -o allexport
|
set -o allexport
|
||||||
SCRIPT_PATH=$(realpath $0)
|
SCRIPT_PATH=$(realpath $0)
|
||||||
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
|
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
|
||||||
@ -75,7 +81,7 @@ pm2 startup
|
|||||||
sudo apt-get install -y certbot
|
sudo apt-get install -y certbot
|
||||||
sudo apt-get install -y python3-certbot-nginx
|
sudo apt-get install -y python3-certbot-nginx
|
||||||
sudo certbot
|
sudo certbot
|
||||||
> Enter email address (used for urgent renewal and security notices) > support@gradido.net
|
> Enter email address (used for urgent renewal and security notices) > e.g. support@supportmail.com
|
||||||
> Please read the Terms of Service at > Y
|
> Please read the Terms of Service at > Y
|
||||||
> Would you be willing, once your first certificate is successfully issued, to > N
|
> Would you be willing, once your first certificate is successfully issued, to > N
|
||||||
> No names were found in your configuration files. Please enter in your domain > stage1.gradido.net
|
> No names were found in your configuration files. Please enter in your domain > stage1.gradido.net
|
||||||
@ -90,7 +96,7 @@ sudo certbot
|
|||||||
# Install logrotate
|
# Install logrotate
|
||||||
sudo apt-get install -y logrotate
|
sudo apt-get install -y logrotate
|
||||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_DIR/logrotate/gradido.conf.template > $SCRIPT_DIR/logrotate/gradido.conf
|
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $SCRIPT_DIR/logrotate/gradido.conf.template > $SCRIPT_DIR/logrotate/gradido.conf
|
||||||
sudo mv $SCRIPT_DIR/logrotate/gradido.conf /etc/logrotate.d/gradido.conf
|
sudo cp $SCRIPT_DIR/logrotate/gradido.conf.template /etc/logrotate.d/gradido.conf
|
||||||
sudo chown root:root /etc/logrotate.d/gradido.conf
|
sudo chown root:root /etc/logrotate.d/gradido.conf
|
||||||
|
|
||||||
# Install mysql autobackup
|
# Install mysql autobackup
|
||||||
@ -131,6 +137,10 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env
|
|||||||
# Configure admin
|
# Configure admin
|
||||||
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
|
envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env
|
||||||
|
|
||||||
|
# create cronjob to delete yarn output in /tmp
|
||||||
|
# crontab -e
|
||||||
|
# hourly job: 0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null
|
||||||
|
# daily job: 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null
|
||||||
# Start gradido
|
# Start gradido
|
||||||
# Note: on first startup some errors will occur - nothing serious
|
# Note: on first startup some errors will occur - nothing serious
|
||||||
./start.sh
|
./start.sh
|
||||||
|
|||||||
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