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).
|
||||
|
||||
#### [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)
|
||||
|
||||
> 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)
|
||||
- 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)
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
|
||||
@ -15,7 +15,10 @@
|
||||
<b-collapse v-model="visible" id="newContribution" class="mt-2">
|
||||
<b-card>
|
||||
<p class="h2 ml-5">{{ $t('contributionLink.contributionLinks') }}</p>
|
||||
<contribution-link-form :contributionLinkData="contributionLinkData" />
|
||||
<contribution-link-form
|
||||
:contributionLinkData="contributionLinkData"
|
||||
@get-contribution-links="$emit('get-contribution-links')"
|
||||
/>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
|
||||
@ -24,6 +27,7 @@
|
||||
v-if="count > 0"
|
||||
:items="items"
|
||||
@editContributionLinkData="editContributionLinkData"
|
||||
@get-contribution-links="$emit('get-contribution-links')"
|
||||
/>
|
||||
<div v-else>{{ $t('contributionLink.noContributionLinks') }}</div>
|
||||
</b-card-text>
|
||||
|
||||
@ -163,7 +163,6 @@ export default {
|
||||
if (this.form.validFrom === null)
|
||||
return this.toastError(this.$t('contributionLink.noStartDate'))
|
||||
if (this.form.validTo === null) return this.toastError(this.$t('contributionLink.noEndDate'))
|
||||
// alert(JSON.stringify(this.form))
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -182,6 +181,8 @@ export default {
|
||||
this.link = result.data.createContributionLink.link
|
||||
this.toastSuccess(this.link)
|
||||
this.onReset()
|
||||
this.$root.$emit('bv::toggle::collapse', 'newContribution')
|
||||
this.$emit('get-contribution-links')
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
|
||||
@ -95,7 +95,7 @@ describe('ContributionLinkList', () => {
|
||||
})
|
||||
|
||||
it('toasts a success message', () => {
|
||||
expect(toastSuccessSpy).toBeCalledWith('TODO: request message deleted ')
|
||||
expect(toastSuccessSpy).toBeCalledWith('contributionLink.deleted')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="contribution-link-list">
|
||||
<b-table striped hover :items="items" :fields="fields">
|
||||
<template #cell(delete)>
|
||||
<template #cell(delete)="data">
|
||||
<b-button
|
||||
variant="danger"
|
||||
size="md"
|
||||
class="mr-2 test-delete-link"
|
||||
@click="deleteContributionLink"
|
||||
@click="deleteContributionLink(data.item.id, data.item.name)"
|
||||
>
|
||||
<b-icon icon="trash" variant="light"></b-icon>
|
||||
</b-button>
|
||||
@ -34,7 +34,7 @@
|
||||
<h6 class="mb-0">{{ modalData ? modalData.name : '' }}</h6>
|
||||
</template>
|
||||
<b-card-text>
|
||||
{{ modalData }}
|
||||
{{ modalData.memo ? modalData.memo : '' }}
|
||||
<figure-qr-code :link="modalData ? modalData.link : ''" />
|
||||
</b-card-text>
|
||||
<template #footer>
|
||||
@ -70,23 +70,25 @@ export default {
|
||||
'edit',
|
||||
'show',
|
||||
],
|
||||
modalData: null,
|
||||
modalDataLink: null,
|
||||
modalData: {},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteContributionLink() {
|
||||
this.$bvModal.msgBoxConfirm(this.$t('contributionLink.deleteNow')).then(async (value) => {
|
||||
deleteContributionLink(id, name) {
|
||||
this.$bvModal
|
||||
.msgBoxConfirm(this.$t('contributionLink.deleteNow', { name: name }))
|
||||
.then(async (value) => {
|
||||
if (value)
|
||||
await this.$apollo
|
||||
.mutate({
|
||||
mutation: deleteContributionLink,
|
||||
variables: {
|
||||
id: this.id,
|
||||
id: id,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.toastSuccess('TODO: request message deleted ')
|
||||
this.toastSuccess(this.$t('contributionLink.deleted'))
|
||||
this.$emit('get-contribution-links')
|
||||
})
|
||||
.catch((err) => {
|
||||
this.toastError(err.message)
|
||||
|
||||
@ -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,31 +6,30 @@ const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
creationTransactionList: [
|
||||
creationTransactionList: {
|
||||
contributionCount: 2,
|
||||
contributionList: [
|
||||
{
|
||||
id: 1,
|
||||
amount: 100,
|
||||
balanceDate: 0,
|
||||
creationDate: new Date(),
|
||||
memo: 'Testing',
|
||||
linkedUser: {
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
},
|
||||
amount: 5.8,
|
||||
createdAt: '2022-09-21T11:09:51.000Z',
|
||||
confirmedAt: null,
|
||||
contributionDate: '2022-08-01T00:00:00.000Z',
|
||||
memo: 'für deine Hilfe, Fräulein Rottenmeier',
|
||||
state: 'PENDING',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
amount: 200,
|
||||
balanceDate: 0,
|
||||
creationDate: new Date(),
|
||||
memo: 'Testing 2',
|
||||
linkedUser: {
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
},
|
||||
amount: '47',
|
||||
createdAt: '2022-09-21T11:09:28.000Z',
|
||||
confirmedAt: '2022-09-21T11:09:28.000Z',
|
||||
contributionDate: '2022-08-01T00:00:00.000Z',
|
||||
memo: 'für deine Hilfe, Frau Holle',
|
||||
state: 'CONFIRMED',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const mocks = {
|
||||
@ -43,7 +42,7 @@ const mocks = {
|
||||
|
||||
const propsData = {
|
||||
userId: 1,
|
||||
fields: ['date', 'balance', 'name', 'memo', 'decay'],
|
||||
fields: ['createdAt', 'contributionDate', 'confirmedAt', 'amount', 'memo'],
|
||||
}
|
||||
|
||||
describe('CreationTransactionList', () => {
|
||||
@ -63,7 +62,7 @@ describe('CreationTransactionList', () => {
|
||||
expect.objectContaining({
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
pageSize: 10,
|
||||
order: 'DESC',
|
||||
userId: 1,
|
||||
},
|
||||
|
||||
@ -1,7 +1,44 @@
|
||||
<template>
|
||||
<div class="component-creation-transaction-list">
|
||||
<div class="h3">{{ $t('transactionlist.title') }}</div>
|
||||
<b-table striped hover :fields="fields" :items="items"></b-table>
|
||||
<b-table striped hover :fields="fields" :items="items">
|
||||
<template #cell(contributionDate)="data">
|
||||
<div class="font-weight-bold">
|
||||
{{ $d(new Date(data.item.contributionDate), 'month') }}
|
||||
</div>
|
||||
<div>{{ $d(new Date(data.item.contributionDate)) }}</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<div>
|
||||
<b-pagination
|
||||
pills
|
||||
size="lg"
|
||||
v-model="currentPage"
|
||||
:per-page="perPage"
|
||||
:total-rows="rows"
|
||||
align="center"
|
||||
:hide-ellipsis="true"
|
||||
></b-pagination>
|
||||
<b-button v-b-toggle.collapse-1 variant="light" size="sm">{{ $t('help.help') }}</b-button>
|
||||
<b-collapse id="collapse-1" class="mt-2">
|
||||
<div>
|
||||
{{ $t('transactionlist.submitted') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.submitted') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('transactionlist.period') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.periods') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('transactionlist.confirmed') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.confirmed') }}
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('transactionlist.state') }} {{ $t('math.equals') }}
|
||||
{{ $t('help.transactionlist.state') }}
|
||||
</div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -13,14 +50,37 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
items: [],
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
perPage: 10,
|
||||
fields: [
|
||||
{
|
||||
key: 'creationDate',
|
||||
label: this.$t('transactionlist.date'),
|
||||
key: 'createdAt',
|
||||
label: this.$t('transactionlist.submitted'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'contributionDate',
|
||||
label: this.$t('transactionlist.period'),
|
||||
},
|
||||
{
|
||||
key: 'confirmedAt',
|
||||
label: this.$t('transactionlist.confirmed'),
|
||||
formatter: (value, key, item) => {
|
||||
if (value) {
|
||||
return this.$d(new Date(value))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'state',
|
||||
label: this.$t('transactionlist.state'),
|
||||
},
|
||||
{
|
||||
key: 'amount',
|
||||
label: this.$t('transactionlist.amount'),
|
||||
@ -28,23 +88,8 @@ export default {
|
||||
return `${value} GDD`
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'linkedUser',
|
||||
label: this.$t('transactionlist.community'),
|
||||
formatter: (value, key, item) => {
|
||||
return `${value.firstName} ${value.lastName}`
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: this.$t('transactionlist.memo') },
|
||||
{
|
||||
key: 'balanceDate',
|
||||
label: this.$t('transactionlist.balanceDate'),
|
||||
formatter: (value, key, item) => {
|
||||
return this.$d(new Date(value))
|
||||
},
|
||||
},
|
||||
],
|
||||
items: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -53,14 +98,15 @@ export default {
|
||||
.query({
|
||||
query: creationTransactionList,
|
||||
variables: {
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.perPage,
|
||||
order: 'DESC',
|
||||
userId: parseInt(this.userId),
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
this.items = result.data.creationTransactionList
|
||||
this.rows = result.data.creationTransactionList.contributionCount
|
||||
this.items = result.data.creationTransactionList.contributionList
|
||||
})
|
||||
.catch((error) => {
|
||||
this.toastError(error.message)
|
||||
@ -70,5 +116,10 @@ export default {
|
||||
created() {
|
||||
this.getTransactions()
|
||||
},
|
||||
watch: {
|
||||
currentPage() {
|
||||
this.getTransactions()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(editCreation)="row">
|
||||
<div v-if="$store.state.moderator.id !== row.item.userId">
|
||||
<b-button
|
||||
v-if="row.item.moderator"
|
||||
variant="info"
|
||||
@ -21,11 +22,32 @@
|
||||
>
|
||||
<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 #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-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 #row-details="row">
|
||||
<row-details
|
||||
@ -33,10 +55,10 @@
|
||||
type="show-creation"
|
||||
slotName="show-creation"
|
||||
:index="0"
|
||||
@row-toggle-details="rowToggleDetails"
|
||||
@row-toggle-details="rowToggleDetails(row, 0)"
|
||||
>
|
||||
<template #show-creation>
|
||||
<div>
|
||||
<div v-if="row.item.moderator">
|
||||
<edit-creation-formular
|
||||
type="singleCreation"
|
||||
:creation="row.item.creation"
|
||||
@ -44,6 +66,12 @@
|
||||
:row="row"
|
||||
:creationUserData="creationUserData"
|
||||
@update-creation-data="updateCreationData"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<contribution-messages-list
|
||||
:contributionId="row.item.id"
|
||||
@update-state="updateState"
|
||||
@update-user-data="updateUserData"
|
||||
/>
|
||||
</div>
|
||||
@ -58,6 +86,7 @@
|
||||
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
|
||||
import RowDetails from '../RowDetails.vue'
|
||||
import EditCreationFormular from '../EditCreationFormular.vue'
|
||||
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
|
||||
|
||||
export default {
|
||||
name: 'OpenCreationsTable',
|
||||
@ -65,6 +94,7 @@ export default {
|
||||
components: {
|
||||
EditCreationFormular,
|
||||
RowDetails,
|
||||
ContributionMessagesList,
|
||||
},
|
||||
props: {
|
||||
items: {
|
||||
@ -98,6 +128,9 @@ export default {
|
||||
updateUserData(rowItem, newCreation) {
|
||||
rowItem.creation = newCreation
|
||||
},
|
||||
updateState(id) {
|
||||
this.$emit('update-state', id)
|
||||
},
|
||||
},
|
||||
}
|
||||
</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
|
||||
userId: $userId
|
||||
) {
|
||||
contributionCount
|
||||
contributionList {
|
||||
id
|
||||
amount
|
||||
balanceDate
|
||||
creationDate
|
||||
createdAt
|
||||
confirmedAt
|
||||
contributionDate
|
||||
memo
|
||||
linkedUser {
|
||||
firstName
|
||||
lastName
|
||||
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
|
||||
firstName
|
||||
lastName
|
||||
userId
|
||||
email
|
||||
amount
|
||||
memo
|
||||
date
|
||||
moderator
|
||||
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",
|
||||
"create": "Anlegen",
|
||||
"cycle": "Zyklus",
|
||||
"deleteNow": "Automatische Creations wirklich löschen?",
|
||||
"deleted": "Automatische Schöpfung gelöscht!",
|
||||
"deleteNow": "Automatische Creations '{name}' wirklich löschen?",
|
||||
"maximumAmount": "maximaler Betrag",
|
||||
"maxPerCycle": "Wiederholungen",
|
||||
"memo": "Nachricht",
|
||||
@ -69,15 +70,32 @@
|
||||
},
|
||||
"short_hash": "({shortHash})"
|
||||
},
|
||||
"form": {
|
||||
"cancel": "Abbrechen",
|
||||
"submit": "Senden"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"help": {
|
||||
"help": "Hilfe",
|
||||
"transactionlist": {
|
||||
"confirmed": "Wann wurde es von einem Moderator / Admin bestätigt.",
|
||||
"periods": "Für welchen Zeitraum wurde vom Mitglied eingereicht.",
|
||||
"state": "[PENDING = eingereicht, DELETED = gelöscht, IN_PROGRESS = im Dialog mit Moderator, DENIED = abgelehnt, CONFIRMED = bestätigt]",
|
||||
"submitted": "Wann wurde es vom Mitglied eingereicht"
|
||||
}
|
||||
},
|
||||
"hide_details": "Details verbergen",
|
||||
"lastname": "Nachname",
|
||||
"math": {
|
||||
"colon": ":",
|
||||
"equals": "=",
|
||||
"exclaim": "!",
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"message": {
|
||||
"request": "Die Anfrage wurde gesendet."
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
"name": "Name",
|
||||
@ -126,10 +144,11 @@
|
||||
},
|
||||
"transactionlist": {
|
||||
"amount": "Betrag",
|
||||
"balanceDate": "Schöpfungsdatum",
|
||||
"community": "Gemeinschaft",
|
||||
"date": "Datum",
|
||||
"confirmed": "Bestätigt",
|
||||
"memo": "Nachricht",
|
||||
"period": "Zeitraum",
|
||||
"state": "Status",
|
||||
"submitted": "Eingereicht",
|
||||
"title": "Alle geschöpften Transaktionen für den Nutzer"
|
||||
},
|
||||
"undelete_user": "Nutzer wiederherstellen",
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
"contributionLinks": "Contribution Links",
|
||||
"create": "Create",
|
||||
"cycle": "Cycle",
|
||||
"deleteNow": "Do you really delete automatic creations?",
|
||||
"deleted": "Automatic creation deleted!",
|
||||
"deleteNow": "Do you really delete automatic creations '{name}'?",
|
||||
"maximumAmount": "Maximum amount",
|
||||
"maxPerCycle": "Repetition",
|
||||
"memo": "Memo",
|
||||
@ -69,15 +70,32 @@
|
||||
},
|
||||
"short_hash": "({shortHash})"
|
||||
},
|
||||
"form": {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Send"
|
||||
},
|
||||
"GDD": "GDD",
|
||||
"help": {
|
||||
"help": "Help",
|
||||
"transactionlist": {
|
||||
"confirmed": "When was it confirmed by a moderator / admin.",
|
||||
"periods": "For what period was it submitted by the member.",
|
||||
"state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = denied, CONFIRMED = confirmed]",
|
||||
"submitted": "When was it submitted by the member"
|
||||
}
|
||||
},
|
||||
"hide_details": "Hide details",
|
||||
"lastname": "Lastname",
|
||||
"math": {
|
||||
"colon": ":",
|
||||
"equals": "=",
|
||||
"exclaim": "!",
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"message": {
|
||||
"request": "Request has been sent."
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
"name": "Name",
|
||||
@ -126,10 +144,11 @@
|
||||
},
|
||||
"transactionlist": {
|
||||
"amount": "Amount",
|
||||
"balanceDate": "Creation date",
|
||||
"community": "Community",
|
||||
"date": "Date",
|
||||
"confirmed": "Confirmed",
|
||||
"memo": "Message",
|
||||
"period": "Period",
|
||||
"state": "State",
|
||||
"submitted": "Submitted",
|
||||
"title": "All creation-transactions for the user"
|
||||
},
|
||||
"undelete_user": "Undelete User",
|
||||
|
||||
@ -14,21 +14,23 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
userId: 99,
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 2,
|
||||
moderator: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
userId: 100,
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergattert',
|
||||
date: new Date(),
|
||||
moderator: 2,
|
||||
moderator: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -41,6 +43,15 @@ const mocks = {
|
||||
$d: jest.fn((d) => d),
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
state: {
|
||||
moderator: {
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
isAdmin: '2022-08-30T07:41:31.000Z',
|
||||
id: 263,
|
||||
language: 'de',
|
||||
},
|
||||
},
|
||||
},
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
:fields="fields"
|
||||
@remove-creation="removeCreation"
|
||||
@show-overlay="showOverlay"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -93,6 +94,10 @@ export default {
|
||||
this.overlay = true
|
||||
this.item = item
|
||||
},
|
||||
updateState(id) {
|
||||
this.pendingCreations.find((obj) => obj.id === id).messagesCount++
|
||||
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS'
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
|
||||
@ -28,7 +28,11 @@
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
<contribution-link :items="items" :count="count" />
|
||||
<contribution-link
|
||||
:items="items"
|
||||
:count="count"
|
||||
@get-contribution-links="getContributionLinks"
|
||||
/>
|
||||
<community-statistic class="mt-5" v-model="statistics" />
|
||||
</div>
|
||||
</template>
|
||||
@ -89,7 +93,6 @@ export default {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: communityStatistics,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.statistics.totalUsers = result.data.communityStatistics.totalUsers
|
||||
|
||||
@ -10,6 +10,7 @@ const authLink = new ApolloLink((operation, forward) => {
|
||||
operation.setContext({
|
||||
headers: {
|
||||
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
|
||||
clientRequestTime: new Date().toString(),
|
||||
},
|
||||
})
|
||||
return forward(operation).map((response) => {
|
||||
|
||||
@ -94,6 +94,7 @@ describe('apolloProvider', () => {
|
||||
expect(setContextMock).toBeCalledWith({
|
||||
headers: {
|
||||
Authorization: 'Bearer some-token',
|
||||
clientRequestTime: expect.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -109,6 +110,7 @@ describe('apolloProvider', () => {
|
||||
expect(setContextMock).toBeCalledWith({
|
||||
headers: {
|
||||
Authorization: '',
|
||||
clientRequestTime: expect.any(String),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v9.2022-07-07
|
||||
CONFIG_VERSION=v10.2022-09-20
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
@ -37,6 +37,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=false
|
||||
EMAIL_TEST_MODUS=false
|
||||
EMAIL_TEST_RECEIVER=stage1@gradido.net
|
||||
EMAIL_USERNAME=gradido_email
|
||||
EMAIL_SENDER=info@gradido.net
|
||||
EMAIL_PASSWORD=xxx
|
||||
|
||||
@ -36,6 +36,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a
|
||||
|
||||
# EMail
|
||||
EMAIL=$EMAIL
|
||||
EMAIL_TEST_MODUS=$EMAIL_TEST_MODUS
|
||||
EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER
|
||||
EMAIL_USERNAME=$EMAIL_USERNAME
|
||||
EMAIL_SENDER=$EMAIL_SENDER
|
||||
EMAIL_PASSWORD=$EMAIL_PASSWORD
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.1",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"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",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||
"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": {
|
||||
"@types/jest": "^27.0.2",
|
||||
|
||||
@ -33,6 +33,8 @@ export enum RIGHTS {
|
||||
LIST_CONTRIBUTION_LINKS = 'LIST_CONTRIBUTION_LINKS',
|
||||
COMMUNITY_STATISTICS = 'COMMUNITY_STATISTICS',
|
||||
SEARCH_ADMIN_USERS = 'SEARCH_ADMIN_USERS',
|
||||
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
|
||||
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
|
||||
// Admin
|
||||
SEARCH_USERS = 'SEARCH_USERS',
|
||||
SET_USER_ROLE = 'SET_USER_ROLE',
|
||||
@ -50,4 +52,5 @@ export enum RIGHTS {
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_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.LIST_CONTRIBUTION_LINKS,
|
||||
RIGHTS.COMMUNITY_STATISTICS,
|
||||
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
|
||||
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
|
||||
])
|
||||
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights
|
||||
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
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
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v9.2022-07-07',
|
||||
EXPECTED: 'v10.2022-09-20',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -67,6 +67,8 @@ const loginServer = {
|
||||
|
||||
const email = {
|
||||
EMAIL: process.env.EMAIL === 'true' || false,
|
||||
EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false',
|
||||
EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net',
|
||||
EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email',
|
||||
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
|
||||
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx',
|
||||
|
||||
@ -32,6 +32,7 @@ export class EventRegister extends EventBasicUserId {}
|
||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||
export class EventInactiveAccount extends EventBasicUserId {}
|
||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
|
||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||
export class EventLogin extends EventBasicUserId {}
|
||||
@ -113,6 +114,15 @@ export class Event {
|
||||
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 {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.CONFIRM_EMAIL
|
||||
|
||||
@ -5,6 +5,7 @@ export enum EventProtocolType {
|
||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||
SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL',
|
||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||
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 this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey
|
||||
const userRepository = await getCustomRepository(UserRepository)
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
try {
|
||||
const user = await userRepository.findByPubkeyHex(context.pubKey)
|
||||
context.user = user
|
||||
|
||||
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 Decimal from 'decimal.js-light'
|
||||
import { Contribution as dbContribution } from '@entity/Contribution'
|
||||
import { User } from './User'
|
||||
import { User } from '@entity/User'
|
||||
|
||||
@ObjectType()
|
||||
export class Contribution {
|
||||
constructor(contribution: dbContribution, user: User) {
|
||||
constructor(contribution: dbContribution, user?: User | null) {
|
||||
this.id = contribution.id
|
||||
this.firstName = user ? user.firstName : null
|
||||
this.lastName = user ? user.lastName : null
|
||||
@ -16,6 +16,8 @@ export class Contribution {
|
||||
this.confirmedAt = contribution.confirmedAt
|
||||
this.confirmedBy = contribution.confirmedBy
|
||||
this.contributionDate = contribution.contributionDate
|
||||
this.state = contribution.contributionStatus
|
||||
this.messagesCount = contribution.messages ? contribution.messages.length : 0
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
@ -47,6 +49,12 @@ export class Contribution {
|
||||
|
||||
@Field(() => Date)
|
||||
contributionDate: Date
|
||||
|
||||
@Field(() => Number)
|
||||
messagesCount: number
|
||||
|
||||
@Field(() => String)
|
||||
state: string
|
||||
}
|
||||
|
||||
@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()
|
||||
export class UnconfirmedContribution {
|
||||
constructor(contribution: Contribution, user: User, creations: Decimal[]) {
|
||||
constructor(contribution: Contribution, user: User | undefined, creations: Decimal[]) {
|
||||
this.id = contribution.id
|
||||
this.userId = contribution.userId
|
||||
this.amount = contribution.amount
|
||||
@ -13,8 +13,11 @@ export class UnconfirmedContribution {
|
||||
this.date = contribution.contributionDate
|
||||
this.firstName = user ? user.firstName : ''
|
||||
this.lastName = user ? user.lastName : ''
|
||||
this.email = user ? user.email : ''
|
||||
this.email = user ? user.emailContact.email : ''
|
||||
this.moderator = contribution.moderatorId
|
||||
this.creation = creations
|
||||
this.state = contribution.contributionStatus
|
||||
this.messageCount = contribution.messages ? contribution.messages.length : 0
|
||||
}
|
||||
|
||||
@Field(() => String)
|
||||
@ -46,4 +49,10 @@ export class UnconfirmedContribution {
|
||||
|
||||
@Field(() => [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 Decimal from 'decimal.js-light'
|
||||
import { FULL_CREATION_AVAILABLE } from '../resolver/const/const'
|
||||
import { UserContact } from './UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class User {
|
||||
@ -10,12 +11,16 @@ export class User {
|
||||
this.id = user.id
|
||||
this.gradidoID = user.gradidoID
|
||||
this.alias = user.alias
|
||||
this.email = user.email
|
||||
this.emailId = user.emailId
|
||||
if (user.emailContact) {
|
||||
this.email = user.emailContact.email
|
||||
this.emailContact = new UserContact(user.emailContact)
|
||||
this.emailChecked = user.emailContact.emailChecked
|
||||
}
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.deletedAt = user.deletedAt
|
||||
this.createdAt = user.createdAt
|
||||
this.emailChecked = user.emailChecked
|
||||
this.language = user.language
|
||||
this.publisherId = user.publisherId
|
||||
this.isAdmin = user.isAdmin
|
||||
@ -34,12 +39,18 @@ export class User {
|
||||
gradidoID: string
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
alias: string
|
||||
alias?: string
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
emailId: number | null
|
||||
|
||||
// TODO privacy issue here
|
||||
@Field(() => String)
|
||||
@Field(() => String, { nullable: true })
|
||||
email: string
|
||||
|
||||
@Field(() => UserContact)
|
||||
emailContact: UserContact
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
firstName: string | null
|
||||
|
||||
|
||||
@ -6,11 +6,11 @@ import { User } from '@entity/User'
|
||||
export class UserAdmin {
|
||||
constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) {
|
||||
this.userId = user.id
|
||||
this.email = user.email
|
||||
this.email = user.emailContact.email
|
||||
this.firstName = user.firstName
|
||||
this.lastName = user.lastName
|
||||
this.creation = creation
|
||||
this.emailChecked = user.emailChecked
|
||||
this.emailChecked = user.emailContact.emailChecked
|
||||
this.hasElopage = hasElopage
|
||||
this.deletedAt = user.deletedAt
|
||||
this.emailConfirmationSend = emailConfirmationSend
|
||||
|
||||
56
backend/src/graphql/model/UserContact.ts
Normal file
56
backend/src/graphql/model/UserContact.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { ObjectType, Field } from 'type-graphql'
|
||||
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||
|
||||
@ObjectType()
|
||||
export class UserContact {
|
||||
constructor(userContact: dbUserContact) {
|
||||
this.id = userContact.id
|
||||
this.type = userContact.type
|
||||
this.userId = userContact.userId
|
||||
this.email = userContact.email
|
||||
// this.emailVerificationCode = userContact.emailVerificationCode
|
||||
this.emailOptInTypeId = userContact.emailOptInTypeId
|
||||
this.emailResendCount = userContact.emailResendCount
|
||||
this.emailChecked = userContact.emailChecked
|
||||
this.phone = userContact.phone
|
||||
this.createdAt = userContact.createdAt
|
||||
this.updatedAt = userContact.updatedAt
|
||||
this.deletedAt = userContact.deletedAt
|
||||
}
|
||||
|
||||
@Field(() => Number)
|
||||
id: number
|
||||
|
||||
@Field(() => String)
|
||||
type: string
|
||||
|
||||
@Field(() => Number)
|
||||
userId: number
|
||||
|
||||
@Field(() => String)
|
||||
email: string
|
||||
|
||||
// @Field(() => BigInt, { nullable: true })
|
||||
// emailVerificationCode: BigInt | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
emailOptInTypeId: number | null
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
emailResendCount: number | null
|
||||
|
||||
@Field(() => Boolean)
|
||||
emailChecked: boolean
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
phone: string | null
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
updatedAt: Date | null
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
deletedAt: Date | null
|
||||
}
|
||||
@ -40,6 +40,7 @@ import Decimal from 'decimal.js-light'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
|
||||
// mock account activation email to avoid console spam
|
||||
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 testEnv: any
|
||||
|
||||
@ -1117,7 +1126,9 @@ describe('AdminResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Could not find user with email: bob@baumeister.de')],
|
||||
errors: [
|
||||
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
@ -1450,6 +1461,20 @@ describe('AdminResolver', () => {
|
||||
expect(transaction[0].linkedUserId).toEqual(null)
|
||||
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', () => {
|
||||
@ -1493,6 +1518,7 @@ describe('AdminResolver', () => {
|
||||
)
|
||||
await expect(r2).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
// data: { confirmContribution: true },
|
||||
errors: [new GraphQLError('Creation was not successful.')],
|
||||
}),
|
||||
)
|
||||
|
||||
@ -4,8 +4,6 @@ import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type
|
||||
import {
|
||||
getCustomRepository,
|
||||
IsNull,
|
||||
Not,
|
||||
ObjectLiteral,
|
||||
getConnection,
|
||||
In,
|
||||
MoreThan,
|
||||
@ -17,6 +15,7 @@ import { AdminCreateContributions } from '@model/AdminCreateContributions'
|
||||
import { AdminUpdateContribution } from '@model/AdminUpdateContribution'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
import { ContributionLinkList } from '@model/ContributionLinkList'
|
||||
import { Contribution } from '@model/Contribution'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { UserRepository } from '@repository/User'
|
||||
import AdminCreateContributionArgs from '@arg/AdminCreateContributionArgs'
|
||||
@ -25,14 +24,11 @@ import SearchUsersArgs from '@arg/SearchUsersArgs'
|
||||
import ContributionLinkArgs from '@arg/ContributionLinkArgs'
|
||||
import { Transaction as DbTransaction } from '@entity/Transaction'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
import { Transaction } from '@model/Transaction'
|
||||
import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionRepository } from '@repository/Transaction'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { User } from '@model/User'
|
||||
import { TransactionTypeId } from '@enum/TransactionTypeId'
|
||||
@ -43,8 +39,7 @@ import { Decay } from '@model/Decay'
|
||||
import Paginated from '@arg/Paginated'
|
||||
import TransactionLinkFilters from '@arg/TransactionLinkFilters'
|
||||
import { Order } from '@enum/Order'
|
||||
import { communityUser } from '@/util/communityUser'
|
||||
import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import CONFIG from '@/config'
|
||||
@ -62,6 +57,14 @@ import {
|
||||
MEMO_MAX_CHARS,
|
||||
MEMO_MIN_CHARS,
|
||||
} from './const/const'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
|
||||
import ContributionMessageArgs from '@arg/ContributionMessageArgs'
|
||||
import { ContributionMessageType } from '@enum/MessageType'
|
||||
import { ContributionMessage } from '@model/ContributionMessage'
|
||||
import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail'
|
||||
import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail'
|
||||
import { ContributionListResult } from '../model/Contribution'
|
||||
|
||||
// const EMAIL_OPT_IN_REGISTER = 1
|
||||
// const EMAIL_OPT_UNKNOWN = 3 // elopage?
|
||||
@ -75,24 +78,12 @@ export class AdminResolver {
|
||||
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
|
||||
): Promise<SearchUsersResult> {
|
||||
const userRepository = getCustomRepository(UserRepository)
|
||||
|
||||
const filterCriteria: ObjectLiteral[] = []
|
||||
if (filters) {
|
||||
if (filters.byActivated !== null) {
|
||||
filterCriteria.push({ emailChecked: filters.byActivated })
|
||||
}
|
||||
|
||||
if (filters.byDeleted !== null) {
|
||||
filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||
}
|
||||
}
|
||||
|
||||
const userFields = [
|
||||
'id',
|
||||
'firstName',
|
||||
'lastName',
|
||||
'email',
|
||||
'emailChecked',
|
||||
'emailId',
|
||||
'emailContact',
|
||||
'deletedAt',
|
||||
'isAdmin',
|
||||
]
|
||||
@ -101,7 +92,7 @@ export class AdminResolver {
|
||||
return 'user.' + fieldName
|
||||
}),
|
||||
searchText,
|
||||
filterCriteria,
|
||||
filters,
|
||||
currentPage,
|
||||
pageSize,
|
||||
)
|
||||
@ -118,32 +109,18 @@ export class AdminResolver {
|
||||
const adminUsers = await Promise.all(
|
||||
users.map(async (user) => {
|
||||
let emailConfirmationSend = ''
|
||||
if (!user.emailChecked) {
|
||||
const emailOptIn = await LoginEmailOptIn.findOne(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
order: {
|
||||
updatedAt: 'DESC',
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
select: ['updatedAt', 'createdAt'],
|
||||
},
|
||||
)
|
||||
if (emailOptIn) {
|
||||
if (emailOptIn.updatedAt) {
|
||||
emailConfirmationSend = emailOptIn.updatedAt.toISOString()
|
||||
if (!user.emailContact.emailChecked) {
|
||||
if (user.emailContact.updatedAt) {
|
||||
emailConfirmationSend = user.emailContact.updatedAt.toISOString()
|
||||
} else {
|
||||
emailConfirmationSend = emailOptIn.createdAt.toISOString()
|
||||
}
|
||||
emailConfirmationSend = user.emailContact.createdAt.toISOString()
|
||||
}
|
||||
}
|
||||
const userCreations = creations.find((c) => c.id === user.id)
|
||||
const adminUser = new UserAdmin(
|
||||
user,
|
||||
userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE,
|
||||
await hasElopageBuys(user.email),
|
||||
await hasElopageBuys(user.emailContact.email),
|
||||
emailConfirmationSend,
|
||||
)
|
||||
return adminUser
|
||||
@ -239,24 +216,39 @@ export class AdminResolver {
|
||||
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<Decimal[]> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
if (!user) {
|
||||
logger.info(
|
||||
`adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`,
|
||||
)
|
||||
const emailContact = await UserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find user with email: ${email}`)
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
if (emailContact.deletedAt) {
|
||||
logger.error('This emailContact was deleted. Cannot create a contribution.')
|
||||
throw new Error('This emailContact was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (emailContact.user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot create a contribution.')
|
||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (!user.emailChecked) {
|
||||
if (!emailContact.emailChecked) {
|
||||
logger.error('Contribution could not be saved, Email is not activated')
|
||||
throw new Error('Contribution could not be saved, Email is not activated')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
logger.trace('moderator: ', moderator.id)
|
||||
const creations = await getUserCreation(user.id)
|
||||
logger.trace('creations', creations)
|
||||
const creations = await getUserCreation(emailContact.userId)
|
||||
logger.trace('creations:', creations)
|
||||
const creationDateObj = new Date(creationDate)
|
||||
logger.trace('creationDateObj:', creationDateObj)
|
||||
validateContribution(creations, amount, creationDateObj)
|
||||
const contribution = Contribution.create()
|
||||
contribution.userId = user.id
|
||||
const contribution = DbContribution.create()
|
||||
contribution.userId = emailContact.userId
|
||||
contribution.amount = amount
|
||||
contribution.createdAt = new Date()
|
||||
contribution.contributionDate = creationDateObj
|
||||
@ -266,8 +258,8 @@ export class AdminResolver {
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
logger.trace('contribution to save', contribution)
|
||||
await Contribution.save(contribution)
|
||||
return getUserCreation(user.id)
|
||||
await DbContribution.save(contribution)
|
||||
return getUserCreation(emailContact.userId)
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
|
||||
@ -303,29 +295,43 @@ export class AdminResolver {
|
||||
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
|
||||
@Ctx() context: Context,
|
||||
): Promise<AdminUpdateContribution> {
|
||||
const user = await dbUser.findOne({ email }, { withDeleted: true })
|
||||
const emailContact = await UserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
}
|
||||
const user = emailContact.user
|
||||
if (!user) {
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
logger.error(`Could not find User to emailContact: ${email}`)
|
||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
logger.error(`User was deleted (${email})`)
|
||||
throw new Error(`User was deleted (${email})`)
|
||||
}
|
||||
|
||||
const moderator = getUser(context)
|
||||
|
||||
const contributionToUpdate = await Contribution.findOne({
|
||||
const contributionToUpdate = await DbContribution.findOne({
|
||||
where: { id, confirmedAt: IsNull() },
|
||||
})
|
||||
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id.')
|
||||
throw new Error('No contribution found to given id.')
|
||||
}
|
||||
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
logger.error('user of the pending contribution and send user does not correspond')
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
if (contributionToUpdate.moderatorId === null) {
|
||||
logger.error('An admin is not allowed to update a user contribution.')
|
||||
throw new Error('An admin is not allowed to update a user contribution.')
|
||||
}
|
||||
|
||||
@ -343,7 +349,7 @@ export class AdminResolver {
|
||||
contributionToUpdate.moderatorId = moderator.id
|
||||
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
|
||||
|
||||
await Contribution.save(contributionToUpdate)
|
||||
await DbContribution.save(contributionToUpdate)
|
||||
const result = new AdminUpdateContribution()
|
||||
result.amount = amount
|
||||
result.memo = contributionToUpdate.memo
|
||||
@ -357,39 +363,44 @@ export class AdminResolver {
|
||||
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||
@Query(() => [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) {
|
||||
return []
|
||||
}
|
||||
|
||||
const userIds = contributions.map((p) => p.userId)
|
||||
const userCreations = await getUserCreations(userIds)
|
||||
const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true })
|
||||
const users = await dbUser.find({
|
||||
where: { id: In(userIds) },
|
||||
withDeleted: true,
|
||||
relations: ['emailContact'],
|
||||
})
|
||||
|
||||
return contributions.map((contribution) => {
|
||||
const user = users.find((u) => u.id === contribution.userId)
|
||||
const creation = userCreations.find((c) => c.id === contribution.userId)
|
||||
|
||||
return {
|
||||
id: contribution.id,
|
||||
userId: contribution.userId,
|
||||
date: contribution.contributionDate,
|
||||
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,
|
||||
}
|
||||
return new UnconfirmedContribution(
|
||||
contribution,
|
||||
user,
|
||||
creation ? creation.creations : FULL_CREATION_AVAILABLE,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||
@Mutation(() => Boolean)
|
||||
async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise<boolean> {
|
||||
const contribution = await Contribution.findOne(id)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found for given id.')
|
||||
}
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
@ -404,17 +415,24 @@ export class AdminResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const contribution = await Contribution.findOne(id)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found to given id.')
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
logger.error('Moderator can not confirm own contribution')
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
|
||||
const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true })
|
||||
if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
|
||||
}
|
||||
const user = await dbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
if (user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
}
|
||||
const creations = await getUserCreation(contribution.userId, false)
|
||||
validateContribution(creations, contribution.amount, contribution.contributionDate)
|
||||
|
||||
@ -422,7 +440,7 @@ export class AdminResolver {
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||
try {
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
@ -462,10 +480,20 @@ export class AdminResolver {
|
||||
contribution.confirmedBy = moderatorUser.id
|
||||
contribution.transactionId = transaction.id
|
||||
contribution.contributionStatus = ContributionStatus.CONFIRMED
|
||||
await queryRunner.manager.update(Contribution, { id: contribution.id }, contribution)
|
||||
await queryRunner.manager.update(DbContribution, { id: contribution.id }, contribution)
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('creation commited successfuly.')
|
||||
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) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation was not successful: ${e}`)
|
||||
@ -477,56 +505,64 @@ export class AdminResolver {
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.CREATION_TRANSACTION_LIST])
|
||||
@Query(() => [Transaction])
|
||||
@Query(() => ContributionListResult)
|
||||
async creationTransactionList(
|
||||
@Args()
|
||||
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
|
||||
@Arg('userId', () => Int) userId: number,
|
||||
): Promise<Transaction[]> {
|
||||
): Promise<ContributionListResult> {
|
||||
const offset = (currentPage - 1) * pageSize
|
||||
const transactionRepository = getCustomRepository(TransactionRepository)
|
||||
const [userTransactions] = await transactionRepository.findByUserPaged(
|
||||
userId,
|
||||
pageSize,
|
||||
offset,
|
||||
order,
|
||||
true,
|
||||
)
|
||||
const [contributionResult, count] = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('c')
|
||||
.from(DbContribution, 'c')
|
||||
.leftJoinAndSelect('c.user', 'u')
|
||||
.where(`user_id = ${userId}`)
|
||||
.limit(pageSize)
|
||||
.offset(offset)
|
||||
.orderBy('c.created_at', order)
|
||||
.getManyAndCount()
|
||||
|
||||
const user = await dbUser.findOneOrFail({ id: userId })
|
||||
return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
contributionResult.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||
)
|
||||
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
|
||||
}
|
||||
|
||||
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
|
||||
@Mutation(() => Boolean)
|
||||
async sendActivationEmail(@Arg('email') email: string): Promise<boolean> {
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await dbUser.findOneOrFail({ email: email })
|
||||
|
||||
// can be both types: REGISTER and RESET_PASSWORD
|
||||
let optInCode = await LoginEmailOptIn.findOne({
|
||||
where: { userId: user.id },
|
||||
order: { updatedAt: 'DESC' },
|
||||
})
|
||||
|
||||
optInCode = await checkOptInCode(optInCode, user)
|
||||
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||
const user = await findUserByEmail(email)
|
||||
if (!user) {
|
||||
logger.error(`Could not find User to emailContact: ${email}`)
|
||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
logger.error(`User with emailContact: ${email} is deleted.`)
|
||||
throw new Error(`User with emailContact: ${email} is deleted.`)
|
||||
}
|
||||
const emailContact = user.emailContact
|
||||
if (emailContact.deletedAt) {
|
||||
logger.error(`The emailContact: ${email} of htis User is deleted.`)
|
||||
throw new Error(`The emailContact: ${email} of htis User is deleted.`)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountActivationEmail({
|
||||
link: activationLink(optInCode),
|
||||
link: activationLink(emailContact.emailVerificationCode),
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
|
||||
/* uncomment this, when you need the activation link on the console
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Account confirmation link: ${activationLink}`)
|
||||
logger.info(`Account confirmation link: ${activationLink}`)
|
||||
}
|
||||
*/
|
||||
|
||||
return true
|
||||
}
|
||||
@ -696,4 +732,73 @@ export class AdminResolver {
|
||||
logger.debug(`updateContributionLink successful!`)
|
||||
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 { Contribution, ContributionListResult } from '@model/Contribution'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import { User } from '@model/User'
|
||||
import { validateContribution, getUserCreation, updateCreations } from './util/creations'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
|
||||
@ -90,19 +89,23 @@ export class ContributionResolver {
|
||||
userId: number
|
||||
confirmedBy?: FindOperator<number> | null
|
||||
} = { userId: user.id }
|
||||
|
||||
if (filterConfirmed) where.confirmedBy = IsNull()
|
||||
const [contributions, count] = await dbContribution.findAndCount({
|
||||
where,
|
||||
order: {
|
||||
createdAt: order,
|
||||
},
|
||||
withDeleted: true,
|
||||
skip: (currentPage - 1) * pageSize,
|
||||
take: pageSize,
|
||||
})
|
||||
|
||||
const [contributions, count] = await getConnection()
|
||||
.createQueryBuilder()
|
||||
.select('c')
|
||||
.from(dbContribution, 'c')
|
||||
.leftJoinAndSelect('c.messages', 'm')
|
||||
.where(where)
|
||||
.orderBy('c.createdAt', order)
|
||||
.limit(pageSize)
|
||||
.offset((currentPage - 1) * pageSize)
|
||||
.getManyAndCount()
|
||||
|
||||
return new ContributionListResult(
|
||||
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()
|
||||
return new ContributionListResult(
|
||||
count,
|
||||
dbContributions.map(
|
||||
(contribution) => new Contribution(contribution, new User(contribution.user)),
|
||||
),
|
||||
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export class GdtResolver {
|
||||
|
||||
try {
|
||||
const resultGDT = await apiGet(
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`,
|
||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
||||
)
|
||||
if (!resultGDT.success) {
|
||||
throw new Error(resultGDT.data)
|
||||
@ -37,7 +37,7 @@ export class GdtResolver {
|
||||
const user = getUser(context)
|
||||
try {
|
||||
const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, {
|
||||
email: user.email,
|
||||
email: user.emailContact.email,
|
||||
})
|
||||
if (!resultGDTSum.success) {
|
||||
throw new Error('Call not successful')
|
||||
|
||||
@ -7,48 +7,47 @@ import { getConnection } from '@dbTools/typeorm'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
@Resolver()
|
||||
export class StatisticsResolver {
|
||||
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
|
||||
@Query(() => CommunityStatistics)
|
||||
async communityStatistics(): Promise<CommunityStatistics> {
|
||||
const allUsers = await DbUser.find({ withDeleted: true })
|
||||
|
||||
let totalUsers = 0
|
||||
let activeUsers = 0
|
||||
let deletedUsers = 0
|
||||
const allUsers = await DbUser.count({ withDeleted: true })
|
||||
const totalUsers = await DbUser.count()
|
||||
const deletedUsers = allUsers - totalUsers
|
||||
|
||||
let totalGradidoAvailable: Decimal = new Decimal(0)
|
||||
let totalGradidoUnbookedDecayed: Decimal = new Decimal(0)
|
||||
|
||||
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,
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
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 queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
})
|
||||
|
||||
const { totalGradidoCreated } = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
|
||||
@ -178,7 +178,7 @@ export class TransactionLinkResolver {
|
||||
logger.info('redeem contribution link...')
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('SERIALIZABLE')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
try {
|
||||
const contributionLink = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
@ -283,7 +283,10 @@ export class TransactionLinkResolver {
|
||||
return true
|
||||
} else {
|
||||
const transactionLink = await dbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId })
|
||||
const linkedUser = await dbUser.findOneOrFail(
|
||||
{ id: transactionLink.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
|
||||
if (user.id === linkedUser.id) {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
|
||||
@ -35,6 +35,8 @@ import Decimal from 'decimal.js-light'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -78,7 +80,7 @@ export const executeTransaction = async (
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
logger.debug(`open Transaction to write...`)
|
||||
try {
|
||||
// transaction
|
||||
@ -148,12 +150,24 @@ export const executeTransaction = async (
|
||||
senderLastName: sender.lastName,
|
||||
recipientFirstName: recipient.firstName,
|
||||
recipientLastName: recipient.lastName,
|
||||
email: recipient.email,
|
||||
senderEmail: sender.email,
|
||||
email: recipient.emailContact.email,
|
||||
senderEmail: sender.emailContact.email,
|
||||
amount,
|
||||
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`)
|
||||
return true
|
||||
}
|
||||
@ -171,7 +185,7 @@ export class TransactionResolver {
|
||||
const user = getUser(context)
|
||||
|
||||
logger.addContext('user', user.id)
|
||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`)
|
||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
@ -293,16 +307,25 @@ export class TransactionResolver {
|
||||
}
|
||||
|
||||
// validate recipient user
|
||||
const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true })
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
/*
|
||||
const emailContact = await UserContact.findOne({ email }, { withDeleted: true })
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
}
|
||||
*/
|
||||
// const recipientUser = await dbUser.findOne({ id: emailContact.userId })
|
||||
if (!recipientUser) {
|
||||
logger.error(`recipient not known: email=${email}`)
|
||||
throw new Error('recipient not known')
|
||||
logger.error(`unknown recipient to UserContact: email=${email}`)
|
||||
throw new Error('unknown recipient')
|
||||
}
|
||||
if (recipientUser.deletedAt) {
|
||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account was deleted')
|
||||
}
|
||||
if (!recipientUser.emailChecked) {
|
||||
const emailContact = recipientUser.emailContact
|
||||
if (!emailContact.emailChecked) {
|
||||
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account is not activated')
|
||||
}
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers'
|
||||
import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations'
|
||||
import {
|
||||
createUser,
|
||||
setPassword,
|
||||
forgotPassword,
|
||||
updateUserInfos,
|
||||
createContribution,
|
||||
confirmContribution,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { User } from '@entity/User'
|
||||
import CONFIG from '@/config'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
@ -15,13 +21,19 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi
|
||||
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
|
||||
import { printTimeDuration, activationLink } from './UserResolver'
|
||||
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
|
||||
// import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { transactionLinkFactory } from '@/seeds/factory/transactionLink'
|
||||
import { ContributionLink } from '@model/ContributionLink'
|
||||
// import { TransactionLink } from '@entity/TransactionLink'
|
||||
import { TransactionLink } from '@entity/TransactionLink'
|
||||
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import { logger } from '@test/testSetup'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
import { OptInType } from '../enum/OptInType'
|
||||
import { UserContactType } from '../enum/UserContactType'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
|
||||
// import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
|
||||
@ -82,7 +94,7 @@ describe('UserResolver', () => {
|
||||
}
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let emailVerificationCode: string
|
||||
let user: User[]
|
||||
|
||||
beforeAll(async () => {
|
||||
@ -101,11 +113,11 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('valid input data', () => {
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
// let loginEmailOptIn: LoginEmailOptIn[]
|
||||
beforeAll(async () => {
|
||||
user = await User.find()
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
user = await User.find({ relations: ['emailContact'] })
|
||||
// loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailVerificationCode = user[0].emailContact.emailVerificationCode.toString()
|
||||
})
|
||||
|
||||
describe('filling all tables', () => {
|
||||
@ -115,15 +127,16 @@ describe('UserResolver', () => {
|
||||
id: expect.any(Number),
|
||||
gradidoID: expect.any(String),
|
||||
alias: null,
|
||||
email: 'peter@lustig.de',
|
||||
emailContact: expect.any(UserContact), // 'peter@lustig.de',
|
||||
emailId: expect.any(Number),
|
||||
firstName: 'Peter',
|
||||
lastName: 'Lustig',
|
||||
password: '0',
|
||||
pubKey: null,
|
||||
privKey: null,
|
||||
emailHash: expect.any(Buffer),
|
||||
// emailHash: expect.any(Buffer),
|
||||
createdAt: expect.any(Date),
|
||||
emailChecked: false,
|
||||
// emailChecked: false,
|
||||
passphrase: expect.any(String),
|
||||
language: 'de',
|
||||
isAdmin: null,
|
||||
@ -139,18 +152,21 @@ describe('UserResolver', () => {
|
||||
expect(verUUID).toEqual(4)
|
||||
})
|
||||
|
||||
it('creates an email optin', () => {
|
||||
expect(loginEmailOptIn).toEqual([
|
||||
{
|
||||
it('creates an email contact', () => {
|
||||
expect(user[0].emailContact).toEqual({
|
||||
id: expect.any(Number),
|
||||
type: UserContactType.USER_CONTACT_EMAIL,
|
||||
userId: user[0].id,
|
||||
verificationCode: expect.any(String),
|
||||
emailOptInTypeId: 1,
|
||||
email: 'peter@lustig.de',
|
||||
emailChecked: false,
|
||||
emailVerificationCode: expect.any(String),
|
||||
emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER,
|
||||
emailResendCount: 0,
|
||||
phone: null,
|
||||
createdAt: expect.any(Date),
|
||||
resendCount: 0,
|
||||
updatedAt: expect.any(Date),
|
||||
},
|
||||
])
|
||||
deletedAt: null,
|
||||
updatedAt: null,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -159,7 +175,7 @@ describe('UserResolver', () => {
|
||||
it('sends an account activation email', () => {
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
emailOptIn,
|
||||
emailVerificationCode,
|
||||
).replace(/{code}/g, '')
|
||||
expect(sendAccountActivationEmail).toBeCalledWith({
|
||||
link: activationLink,
|
||||
@ -169,6 +185,15 @@ describe('UserResolver', () => {
|
||||
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', () => {
|
||||
@ -208,13 +233,13 @@ describe('UserResolver', () => {
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' },
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
email: 'bibi@bloxberg.de',
|
||||
language: 'de',
|
||||
user: expect.objectContaining({ language: 'de' }),
|
||||
}),
|
||||
]),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -225,10 +250,12 @@ describe('UserResolver', () => {
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
|
||||
})
|
||||
await expect(User.find()).resolves.toEqual(
|
||||
await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
emailContact: expect.objectContaining({
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
}),
|
||||
publisherId: null,
|
||||
}),
|
||||
]),
|
||||
@ -237,38 +264,158 @@ describe('UserResolver', () => {
|
||||
})
|
||||
|
||||
describe('redeem codes', () => {
|
||||
describe('contribution link', () => {
|
||||
let result: any
|
||||
let link: ContributionLink
|
||||
|
||||
describe('contribution link', () => {
|
||||
beforeAll(async () => {
|
||||
// activate account of admin Peter Lustig
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
// make Peter Lustig Admin
|
||||
const peter = await User.findOneOrFail({ id: user[0].id })
|
||||
peter.isAdmin = new Date()
|
||||
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
|
||||
link = await contributionLinkFactory(testEnv, {
|
||||
name: 'Dokumenta 2022',
|
||||
memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022',
|
||||
amount: 200,
|
||||
validFrom: new Date(2022, 5, 18),
|
||||
validTo: new Date(2022, 8, 25),
|
||||
validFrom: actualDate,
|
||||
validTo: futureDate,
|
||||
})
|
||||
|
||||
resetToken()
|
||||
await mutate({
|
||||
result = await mutate({
|
||||
mutation: createUser,
|
||||
variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
it('sets the contribution link id', async () => {
|
||||
await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual(
|
||||
await expect(
|
||||
UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -313,20 +460,23 @@ bei Gradidio sei dabei!`,
|
||||
}
|
||||
|
||||
let result: any
|
||||
let emailOptIn: string
|
||||
let emailVerificationCode: string
|
||||
|
||||
describe('valid optin code and valid password', () => {
|
||||
let newUser: any
|
||||
let newUser: User
|
||||
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'Aa12345_' },
|
||||
variables: { code: emailVerificationCode, password: 'Aa12345_' },
|
||||
})
|
||||
newUser = await User.find()
|
||||
newUser = await User.findOneOrFail(
|
||||
{ id: emailContact.userId },
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -334,11 +484,11 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
it('sets email checked to true', () => {
|
||||
expect(newUser[0].emailChecked).toBeTruthy()
|
||||
expect(newUser.emailContact.emailChecked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('updates the password', () => {
|
||||
expect(newUser[0].password).toEqual('3917921995996627700')
|
||||
expect(newUser.password).toEqual('3917921995996627700')
|
||||
})
|
||||
|
||||
/*
|
||||
@ -360,11 +510,11 @@ bei Gradidio sei dabei!`,
|
||||
describe('no valid password', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({ mutation: createUser, variables: createUserVariables })
|
||||
const loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailOptIn = loginEmailOptIn[0].verificationCode.toString()
|
||||
const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email })
|
||||
emailVerificationCode = emailContact.emailVerificationCode.toString()
|
||||
result = await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { code: emailOptIn, password: 'not-valid' },
|
||||
variables: { code: emailVerificationCode, password: 'not-valid' },
|
||||
})
|
||||
})
|
||||
|
||||
@ -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', () => {
|
||||
@ -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', () => {
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
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', () => {
|
||||
@ -475,6 +640,7 @@ bei Gradidio sei dabei!`,
|
||||
describe('user is in database and wrong password', () => {
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
result = await query({ query: login, variables: { ...variables, password: 'wrong' } })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -482,14 +648,16 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
it('returns an error', () => {
|
||||
expect(
|
||||
query({ query: login, variables: { ...variables, password: 'wrong' } }),
|
||||
).resolves.toEqual(
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
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', () => {
|
||||
let user: User[]
|
||||
|
||||
const variables = {
|
||||
email: 'bibi@bloxberg.de',
|
||||
password: 'Aa12345_',
|
||||
@ -569,6 +739,7 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
beforeAll(async () => {
|
||||
await query({ query: login, variables })
|
||||
user = await User.find()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
@ -595,13 +766,25 @@ 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', () => {
|
||||
const variables = { email: 'bibi@bloxberg.de' }
|
||||
const emailCodeRequestTime = CONFIG.EMAIL_CODE_REQUEST_TIME
|
||||
|
||||
describe('user is not in DB', () => {
|
||||
describe('duration not expired', () => {
|
||||
it('returns true', async () => {
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
@ -612,24 +795,42 @@ bei Gradidio sei dabei!`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('user exists in DB', () => {
|
||||
let result: any
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
let emailContact: UserContact
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await resetEntity(LoginEmailOptIn)
|
||||
result = await mutate({ mutation: forgotPassword, variables })
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
// await resetEntity(LoginEmailOptIn)
|
||||
emailContact = await UserContact.findOneOrFail(variables)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||
})
|
||||
|
||||
describe('duration not expired', () => {
|
||||
it('returns true', async () => {
|
||||
await expect(result).toEqual(
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('duration reset to 0', () => {
|
||||
it('returns true', async () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = 0
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
forgotPassword: true,
|
||||
@ -637,10 +838,11 @@ bei Gradidio sei dabei!`,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('sends reset password email', () => {
|
||||
expect(sendResetPasswordEmail).toBeCalledWith({
|
||||
link: activationLink(loginEmailOptIn[0]),
|
||||
link: activationLink(emailContact.emailVerificationCode),
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
email: 'bibi@bloxberg.de',
|
||||
@ -650,22 +852,27 @@ bei Gradidio sei dabei!`,
|
||||
|
||||
describe('request reset password again', () => {
|
||||
it('thows an error', async () => {
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes minutes ago`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('queryOptIn', () => {
|
||||
let loginEmailOptIn: LoginEmailOptIn[]
|
||||
let emailContact: UserContact
|
||||
|
||||
beforeAll(async () => {
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
loginEmailOptIn = await LoginEmailOptIn.find()
|
||||
emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -680,8 +887,8 @@ bei Gradidio sei dabei!`,
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
// keep Whitspace in error message!
|
||||
new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: {
|
||||
"verificationCode": "not-valid"
|
||||
new GraphQLError(`Could not find any entity of type "UserContact" matching: {
|
||||
"emailVerificationCode": "not-valid"
|
||||
}`),
|
||||
],
|
||||
}),
|
||||
@ -694,7 +901,7 @@ bei Gradidio sei dabei!`,
|
||||
await expect(
|
||||
query({
|
||||
query: queryOptIn,
|
||||
variables: { optIn: loginEmailOptIn[0].verificationCode.toString() },
|
||||
variables: { optIn: emailContact.emailVerificationCode.toString() },
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
@ -766,7 +973,7 @@ bei Gradidio sei dabei!`,
|
||||
})
|
||||
|
||||
describe('language is not valid', () => {
|
||||
it('thows an error', async () => {
|
||||
it('throws an error', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
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', () => {
|
||||
@ -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', () => {
|
||||
@ -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', () => {
|
||||
@ -840,7 +1059,7 @@ bei Gradidio sei dabei!`,
|
||||
)
|
||||
})
|
||||
|
||||
it('can login wtih new password', async () => {
|
||||
it('can login with new password', async () => {
|
||||
await expect(
|
||||
query({
|
||||
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(
|
||||
query({
|
||||
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 { backendLogger as logger } from '@/server/logger'
|
||||
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
|
||||
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
|
||||
import CONFIG from '@/config'
|
||||
import { User } from '@model/User'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
import { UserContact as DbUserContact } from '@entity/UserContact'
|
||||
import { communityDbUser } from '@/util/communityUser'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { ContributionLink as dbContributionLink } from '@entity/ContributionLink'
|
||||
@ -16,7 +16,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs'
|
||||
import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs'
|
||||
import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware'
|
||||
import { OptInType } from '@enum/OptInType'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
|
||||
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
|
||||
import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
|
||||
@ -29,9 +28,12 @@ import {
|
||||
EventLogin,
|
||||
EventRedeemRegister,
|
||||
EventRegister,
|
||||
EventSendAccountMultiRegistrationEmail,
|
||||
EventSendConfirmationEmail,
|
||||
EventActivateAccount,
|
||||
} from '@/event/Event'
|
||||
import { getUserCreation } from './util/creations'
|
||||
import { UserContactType } from '../enum/UserContactType'
|
||||
import { UserRepository } from '@/typeorm/repository/User'
|
||||
import { SearchAdminUsersResult } from '@model/AdminUser'
|
||||
import Paginated from '@arg/Paginated'
|
||||
@ -146,6 +148,7 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[
|
||||
return [encryptionKeyHash, encryptionKey]
|
||||
}
|
||||
|
||||
/*
|
||||
const getEmailHash = (email: string): Buffer => {
|
||||
logger.trace('getEmailHash...')
|
||||
const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES)
|
||||
@ -153,6 +156,7 @@ const getEmailHash = (email: string): Buffer => {
|
||||
logger.debug(`getEmailHash...successful: ${emailHash}`)
|
||||
return emailHash
|
||||
}
|
||||
*/
|
||||
|
||||
const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => {
|
||||
logger.trace('SecretKeyCryptographyEncrypt...')
|
||||
@ -177,6 +181,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B
|
||||
return message
|
||||
}
|
||||
|
||||
const newEmailContact = (email: string, userId: number): DbUserContact => {
|
||||
logger.trace(`newEmailContact...`)
|
||||
const emailContact = new DbUserContact()
|
||||
emailContact.email = email
|
||||
emailContact.userId = userId
|
||||
emailContact.type = UserContactType.USER_CONTACT_EMAIL
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
logger.debug(`newEmailContact...successful: ${emailContact}`)
|
||||
return emailContact
|
||||
}
|
||||
/*
|
||||
const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||
logger.trace('newEmailOptIn...')
|
||||
const emailOptIn = new LoginEmailOptIn()
|
||||
@ -186,7 +203,8 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => {
|
||||
logger.debug(`newEmailOptIn...successful: ${emailOptIn}`)
|
||||
return emailOptIn
|
||||
}
|
||||
|
||||
*/
|
||||
/*
|
||||
// needed by AdminResolver
|
||||
// checks if given code exists and can be resent
|
||||
// if optIn does not exits, it is created
|
||||
@ -226,10 +244,44 @@ export const checkOptInCode = async (
|
||||
logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`)
|
||||
return optInCode
|
||||
}
|
||||
*/
|
||||
export const checkEmailVerificationCode = async (
|
||||
emailContact: DbUserContact,
|
||||
optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER,
|
||||
): Promise<DbUserContact> => {
|
||||
logger.info(`checkEmailVerificationCode... ${emailContact}`)
|
||||
if (emailContact.updatedAt) {
|
||||
if (!canEmailResend(emailContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
throw new Error(
|
||||
`email already sent less than ${printTimeDuration(
|
||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||
)} minutes ago`,
|
||||
)
|
||||
}
|
||||
emailContact.updatedAt = new Date()
|
||||
emailContact.emailResendCount++
|
||||
} else {
|
||||
logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId)
|
||||
emailContact.emailChecked = false
|
||||
emailContact.emailVerificationCode = random(64)
|
||||
}
|
||||
emailContact.emailOptInTypeId = optInType
|
||||
await DbUserContact.save(emailContact).catch(() => {
|
||||
logger.error('Unable to save email verification code= ' + emailContact)
|
||||
throw new Error('Unable to save email verification code.')
|
||||
})
|
||||
logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`)
|
||||
return emailContact
|
||||
}
|
||||
|
||||
export const activationLink = (optInCode: LoginEmailOptIn): string => {
|
||||
logger.debug(`activationLink(${LoginEmailOptIn})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString())
|
||||
export const activationLink = (verificationCode: BigInt): string => {
|
||||
logger.debug(`activationLink(${verificationCode})...`)
|
||||
return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString())
|
||||
}
|
||||
|
||||
const newGradidoID = async (): Promise<string> => {
|
||||
@ -272,15 +324,12 @@ export class UserResolver {
|
||||
): Promise<User> {
|
||||
logger.info(`login with ${email}, ***, ${publisherId} ...`)
|
||||
email = email.trim().toLowerCase()
|
||||
const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => {
|
||||
logger.error(`User with email=${email} does not exists`)
|
||||
throw new Error('No user with this credentials')
|
||||
})
|
||||
const dbUser = await findUserByEmail(email)
|
||||
if (dbUser.deletedAt) {
|
||||
logger.error('The User was permanently deleted in database.')
|
||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
||||
}
|
||||
if (!dbUser.emailChecked) {
|
||||
if (!dbUser.emailContact.emailChecked) {
|
||||
logger.error('The Users email is not validate yet.')
|
||||
throw new Error('User email not validated')
|
||||
}
|
||||
@ -305,7 +354,7 @@ export class UserResolver {
|
||||
logger.debug('login credentials valid...')
|
||||
|
||||
const user = new User(dbUser, await getUserCreation(dbUser.id))
|
||||
logger.debug('user=' + user)
|
||||
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
|
||||
|
||||
// Elopage Status & Stored PublisherId
|
||||
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
|
||||
@ -323,7 +372,7 @@ export class UserResolver {
|
||||
const ev = new EventLogin()
|
||||
ev.userId = user.id
|
||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||
logger.info('successful Login:' + user)
|
||||
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||
return user
|
||||
}
|
||||
|
||||
@ -352,19 +401,20 @@ export class UserResolver {
|
||||
)
|
||||
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
|
||||
// default int publisher_id = 0;
|
||||
const event = new Event()
|
||||
|
||||
// Validate Language (no throw)
|
||||
if (!language || !isLanguage(language)) {
|
||||
language = DEFAULT_LANGUAGE
|
||||
}
|
||||
|
||||
// Validate email unique
|
||||
// check if user with email still exists?
|
||||
email = email.trim().toLowerCase()
|
||||
// TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes
|
||||
const userFound = await DbUser.findOne({ email }, { withDeleted: true })
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${userFound}`)
|
||||
if (await checkEmailExists(email)) {
|
||||
const foundUser = await findUserByEmail(email)
|
||||
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
|
||||
|
||||
if (userFound) {
|
||||
if (foundUser) {
|
||||
// ATTENTION: this logger-message will be exactly expected during tests
|
||||
logger.info(`User already exists with this email=${email}`)
|
||||
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
|
||||
@ -385,6 +435,11 @@ export class UserResolver {
|
||||
lastName,
|
||||
email,
|
||||
})
|
||||
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||
eventProtocol.writeEvent(
|
||||
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||
)
|
||||
logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
@ -395,23 +450,23 @@ export class UserResolver {
|
||||
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
const passphrase = PassphraseGenerate()
|
||||
// const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||
// const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
|
||||
// const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
const emailHash = getEmailHash(email)
|
||||
// const emailHash = getEmailHash(email)
|
||||
const gradidoID = await newGradidoID()
|
||||
|
||||
const eventRegister = new EventRegister()
|
||||
const eventRedeemRegister = new EventRedeemRegister()
|
||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||
const dbUser = new DbUser()
|
||||
|
||||
let dbUser = new DbUser()
|
||||
dbUser.gradidoID = gradidoID
|
||||
dbUser.email = email
|
||||
dbUser.firstName = firstName
|
||||
dbUser.lastName = lastName
|
||||
dbUser.emailHash = emailHash
|
||||
dbUser.language = language
|
||||
dbUser.publisherId = publisherId
|
||||
dbUser.passphrase = passphrase.join(' ')
|
||||
@ -442,25 +497,38 @@ export class UserResolver {
|
||||
// loginUser.pubKey = keyPair[0]
|
||||
// loginUser.privKey = encryptedPrivkey
|
||||
|
||||
const event = new Event()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
try {
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
logger.error('Error while saving dbUser', error)
|
||||
throw new Error('error saving user')
|
||||
})
|
||||
let emailContact = newEmailContact(email, dbUser.id)
|
||||
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
|
||||
logger.error('Error while saving emailContact', error)
|
||||
throw new Error('error saving email user contact')
|
||||
})
|
||||
|
||||
dbUser.emailContact = emailContact
|
||||
dbUser.emailId = emailContact.id
|
||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||
logger.error('Error while updating dbUser', error)
|
||||
throw new Error('error updating user')
|
||||
})
|
||||
|
||||
/*
|
||||
const emailOptIn = newEmailOptIn(dbUser.id)
|
||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
||||
logger.error('Error while saving emailOptIn', error)
|
||||
throw new Error('error saving email opt in')
|
||||
})
|
||||
*/
|
||||
|
||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||
/{optin}/g,
|
||||
emailOptIn.verificationCode.toString(),
|
||||
emailContact.emailVerificationCode.toString(),
|
||||
).replace(/{code}/g, redeemCode ? '/' + redeemCode : '')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -475,8 +543,6 @@ export class UserResolver {
|
||||
eventSendConfirmEmail.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
||||
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||
}
|
||||
@ -493,10 +559,10 @@ export class UserResolver {
|
||||
|
||||
if (redeemCode) {
|
||||
eventRedeemRegister.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||
} else {
|
||||
eventRegister.userId = dbUser.id
|
||||
eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||
}
|
||||
|
||||
return new User(dbUser)
|
||||
@ -507,22 +573,29 @@ export class UserResolver {
|
||||
async forgotPassword(@Arg('email') email: string): Promise<boolean> {
|
||||
logger.info(`forgotPassword(${email})...`)
|
||||
email = email.trim().toLowerCase()
|
||||
const user = await DbUser.findOne({ email })
|
||||
const user = await findUserByEmail(email).catch(() => {
|
||||
logger.warn(`fail on find UserContact per ${email}`)
|
||||
})
|
||||
if (!user) {
|
||||
logger.warn(`no user found with ${email}`)
|
||||
return true
|
||||
}
|
||||
|
||||
// can be both types: REGISTER and RESET_PASSWORD
|
||||
let optInCode = await LoginEmailOptIn.findOne({
|
||||
userId: user.id,
|
||||
})
|
||||
// let optInCode = await LoginEmailOptIn.findOne({
|
||||
// userId: user.id,
|
||||
// })
|
||||
// let optInCode = user.emailContact.emailVerificationCode
|
||||
const dbUserContact = await checkEmailVerificationCode(
|
||||
user.emailContact,
|
||||
OptInType.EMAIL_OPT_IN_RESET_PASSWORD,
|
||||
)
|
||||
|
||||
optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||
logger.info(`optInCode for ${email}=${optInCode}`)
|
||||
// optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD)
|
||||
logger.info(`optInCode for ${email}=${dbUserContact}`)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendResetPasswordEmailMailer({
|
||||
link: activationLink(optInCode),
|
||||
link: activationLink(dbUserContact.emailVerificationCode),
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
@ -532,7 +605,7 @@ export class UserResolver {
|
||||
/* uncomment this, when you need the activation link on the console */
|
||||
// In case EMails are disabled log the activation link for the user
|
||||
if (!emailSent) {
|
||||
logger.debug(`Reset password link: ${activationLink(optInCode)}`)
|
||||
logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`)
|
||||
}
|
||||
logger.info(`forgotPassword(${email}) successful...`)
|
||||
|
||||
@ -548,19 +621,29 @@ export class UserResolver {
|
||||
logger.info(`setPassword(${code}, ***)...`)
|
||||
// Validate Password
|
||||
if (!isPassword(password)) {
|
||||
logger.error('Password entered is lexically invalid')
|
||||
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!',
|
||||
)
|
||||
}
|
||||
|
||||
// Load code
|
||||
/*
|
||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
||||
logger.error('Could not login with emailVerificationCode')
|
||||
throw new Error('Could not login with emailVerificationCode')
|
||||
})
|
||||
logger.debug('optInCode loaded...')
|
||||
*/
|
||||
const userContact = await DbUserContact.findOneOrFail(
|
||||
{ emailVerificationCode: code },
|
||||
{ relations: ['user'] },
|
||||
).catch(() => {
|
||||
logger.error('Could not login with emailVerificationCode')
|
||||
throw new Error('Could not login with emailVerificationCode')
|
||||
})
|
||||
logger.debug('userContact loaded...')
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isOptInValid(optInCode)) {
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
@ -568,14 +651,11 @@ export class UserResolver {
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
}
|
||||
logger.debug('optInCode is valid...')
|
||||
logger.debug('EmailVerificationCode is valid...')
|
||||
|
||||
// load user
|
||||
const user = await DbUser.findOneOrFail({ id: optInCode.userId }).catch(() => {
|
||||
logger.error('Could not find corresponding Login User')
|
||||
throw new Error('Could not find corresponding Login User')
|
||||
})
|
||||
logger.debug('user with optInCode found...')
|
||||
const user = userContact.user
|
||||
logger.debug('user with EmailVerificationCode found...')
|
||||
|
||||
// Generate Passphrase if needed
|
||||
if (!user.passphrase) {
|
||||
@ -595,10 +675,10 @@ export class UserResolver {
|
||||
logger.debug('Passphrase is valid...')
|
||||
|
||||
// Activate EMail
|
||||
user.emailChecked = true
|
||||
userContact.emailChecked = true
|
||||
|
||||
// Update Password
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(user.email, password) // return short and long hash
|
||||
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
|
||||
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
|
||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
|
||||
user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
|
||||
@ -608,7 +688,9 @@ export class UserResolver {
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
const event = new Event()
|
||||
|
||||
try {
|
||||
// Save user
|
||||
@ -616,12 +698,21 @@ export class UserResolver {
|
||||
logger.error('error saving user: ' + error)
|
||||
throw new Error('error saving user: ' + error)
|
||||
})
|
||||
// Save userContact
|
||||
await queryRunner.manager.save(userContact).catch((error) => {
|
||||
logger.error('error saving userContact: ' + error)
|
||||
throw new Error('error saving userContact: ' + error)
|
||||
})
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('User 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) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error('Error on writing User data:' + e)
|
||||
logger.error('Error on writing User and UserContact data:' + e)
|
||||
throw e
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
@ -629,11 +720,11 @@ export class UserResolver {
|
||||
|
||||
// Sign into Klicktipp
|
||||
// TODO do we always signUp the user? How to handle things with old users?
|
||||
if (optInCode.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||
if (userContact.emailOptInTypeId === OptInType.EMAIL_OPT_IN_REGISTER) {
|
||||
try {
|
||||
await klicktippSignIn(user.email, user.language, user.firstName, user.lastName)
|
||||
await klicktippSignIn(userContact.email, user.language, user.firstName, user.lastName)
|
||||
logger.debug(
|
||||
`klicktippSignIn(${user.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||
)
|
||||
} catch (e) {
|
||||
logger.error('Error subscribe to klicktipp:' + e)
|
||||
@ -652,10 +743,10 @@ export class UserResolver {
|
||||
@Query(() => Boolean)
|
||||
async queryOptIn(@Arg('optIn') optIn: string): Promise<boolean> {
|
||||
logger.info(`queryOptIn(${optIn})...`)
|
||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: optIn })
|
||||
logger.debug(`found optInCode=${optInCode}`)
|
||||
const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn })
|
||||
logger.debug(`found optInCode=${userContact}`)
|
||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||
if (!isOptInValid(optInCode)) {
|
||||
if (!isEmailVerificationCodeValid(userContact.updatedAt)) {
|
||||
logger.error(
|
||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||
)
|
||||
@ -703,7 +794,10 @@ export class UserResolver {
|
||||
}
|
||||
|
||||
// TODO: This had some error cases defined - like missing private key. This is no longer checked.
|
||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, password)
|
||||
const oldPasswordHash = SecretKeyCryptographyCreateKey(
|
||||
userEntity.emailContact.email,
|
||||
password,
|
||||
)
|
||||
if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
|
||||
logger.error(`Old password is invalid`)
|
||||
throw new Error(`Old password is invalid`)
|
||||
@ -711,7 +805,10 @@ export class UserResolver {
|
||||
|
||||
const privKey = SecretKeyCryptographyDecrypt(userEntity.privKey, oldPasswordHash[1])
|
||||
logger.debug('oldPassword decrypted...')
|
||||
const newPasswordHash = SecretKeyCryptographyCreateKey(userEntity.email, passwordNew) // return short and long hash
|
||||
const newPasswordHash = SecretKeyCryptographyCreateKey(
|
||||
userEntity.emailContact.email,
|
||||
passwordNew,
|
||||
) // return short and long hash
|
||||
logger.debug('newPasswordHash created...')
|
||||
const encryptedPrivkey = SecretKeyCryptographyEncrypt(privKey, newPasswordHash[1])
|
||||
logger.debug('PrivateKey encrypted...')
|
||||
@ -723,10 +820,11 @@ export class UserResolver {
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('READ UNCOMMITTED')
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
try {
|
||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||
logger.error('error saving user: ' + error)
|
||||
throw new Error('error saving user: ' + error)
|
||||
})
|
||||
|
||||
@ -747,12 +845,8 @@ export class UserResolver {
|
||||
@Query(() => Boolean)
|
||||
async hasElopage(@Ctx() context: Context): Promise<boolean> {
|
||||
logger.info(`hasElopage()...`)
|
||||
const userEntity = context.user
|
||||
if (!userEntity) {
|
||||
logger.info('missing context.user for EloPage-check')
|
||||
return false
|
||||
}
|
||||
const elopageBuys = hasElopageBuys(userEntity.email)
|
||||
const userEntity = getUser(context)
|
||||
const elopageBuys = hasElopageBuys(userEntity.emailContact.email)
|
||||
logger.debug(`has ElopageBuys = ${elopageBuys}`)
|
||||
return elopageBuys
|
||||
}
|
||||
@ -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 timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
||||
// time is given in minutes
|
||||
return timeElapsed <= duration * 60 * 1000
|
||||
}
|
||||
|
||||
*/
|
||||
const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
|
||||
const timeElapsed = Date.now() - new Date(updatedAt).getTime()
|
||||
// time is given in minutes
|
||||
return timeElapsed <= duration * 60 * 1000
|
||||
}
|
||||
/*
|
||||
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||
}
|
||||
|
||||
*/
|
||||
const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => {
|
||||
if (updatedAt == null) {
|
||||
return true
|
||||
}
|
||||
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||
}
|
||||
/*
|
||||
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
||||
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||
}
|
||||
*/
|
||||
const canEmailResend = (updatedAt: Date): boolean => {
|
||||
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||
}
|
||||
|
||||
const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => {
|
||||
if (time > 60) {
|
||||
|
||||
@ -15,14 +15,21 @@ export const validateContribution = (
|
||||
amount: Decimal,
|
||||
creationDate: Date,
|
||||
): void => {
|
||||
logger.trace('isContributionValid', creations, amount, creationDate)
|
||||
logger.trace('isContributionValid: ', creations, amount, creationDate)
|
||||
const index = getCreationIndex(creationDate.getMonth())
|
||||
|
||||
if (index < 0) {
|
||||
logger.error(
|
||||
'No information for available creations with the given creationDate=',
|
||||
creationDate,
|
||||
)
|
||||
throw new Error('No information for available creations for the given date')
|
||||
}
|
||||
|
||||
if (amount.greaterThan(creations[index].toString())) {
|
||||
logger.error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
throw new Error(
|
||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
||||
)
|
||||
@ -41,7 +48,7 @@ export const getUserCreations = async (
|
||||
await queryRunner.connect()
|
||||
|
||||
const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day'
|
||||
logger.trace('getUserCreations dateFilter', dateFilter)
|
||||
logger.trace('getUserCreations dateFilter=', dateFilter)
|
||||
|
||||
const unionString = includePending
|
||||
? `
|
||||
@ -51,6 +58,7 @@ export const getUserCreations = async (
|
||||
AND contribution_date >= ${dateFilter}
|
||||
AND confirmed_at IS NULL AND deleted_at IS NULL`
|
||||
: ''
|
||||
logger.trace('getUserCreations unionString=', unionString)
|
||||
|
||||
const unionQuery = await queryRunner.manager.query(`
|
||||
SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM
|
||||
@ -62,6 +70,7 @@ export const getUserCreations = async (
|
||||
GROUP BY month, userId
|
||||
ORDER BY date DESC
|
||||
`)
|
||||
logger.trace('getUserCreations unionQuery=', unionQuery)
|
||||
|
||||
await queryRunner.release()
|
||||
|
||||
@ -82,6 +91,7 @@ export const getUserCreations = async (
|
||||
export const getUserCreation = async (id: number, includePending = true): Promise<Decimal[]> => {
|
||||
logger.trace('getUserCreation', id, includePending)
|
||||
const creations = await getUserCreations([id], includePending)
|
||||
logger.trace('getUserCreation creations=', creations)
|
||||
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
|
||||
}
|
||||
|
||||
|
||||
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', () => {
|
||||
expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({
|
||||
from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
|
||||
to: 'receiver@mail.org',
|
||||
to: `${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||
cc: 'support@gradido.net',
|
||||
subject: 'Subject',
|
||||
text: 'Text text text',
|
||||
|
||||
@ -19,6 +19,12 @@ export const sendEMail = async (emailDef: {
|
||||
logger.info(`Emails are disabled via config...`)
|
||||
return false
|
||||
}
|
||||
if (CONFIG.EMAIL_TEST_MODUS) {
|
||||
logger.info(
|
||||
`Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
|
||||
)
|
||||
emailDef.to = CONFIG.EMAIL_TEST_RECEIVER
|
||||
}
|
||||
const transporter = createTransport({
|
||||
host: CONFIG.EMAIL_SMTP_URL,
|
||||
port: Number(CONFIG.EMAIL_SMTP_PORT),
|
||||
|
||||
44
backend/src/mailer/sendTransactionLinkRedeemed.test.ts
Normal file
44
backend/src/mailer/sendTransactionLinkRedeemed.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { sendEMail } from './sendEMail'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed'
|
||||
|
||||
jest.mock('./sendEMail', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
sendEMail: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
describe('sendTransactionLinkRedeemedEmail', () => {
|
||||
beforeEach(async () => {
|
||||
await sendTransactionLinkRedeemedEmail({
|
||||
email: 'bibi@bloxberg.de',
|
||||
senderFirstName: 'Peter',
|
||||
senderLastName: 'Lustig',
|
||||
recipientFirstName: 'Bibi',
|
||||
recipientLastName: 'Bloxberg',
|
||||
senderEmail: 'peter@lustig.de',
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen Dank dass Du dabei bist',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
|
||||
it('calls sendEMail', () => {
|
||||
expect(sendEMail).toBeCalledWith({
|
||||
to: `Bibi Bloxberg <bibi@bloxberg.de>`,
|
||||
subject: 'Gradido-Link wurde eingelöst',
|
||||
text:
|
||||
expect.stringContaining('Hallo Bibi Bloxberg') &&
|
||||
expect.stringContaining(
|
||||
'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.',
|
||||
) &&
|
||||
expect.stringContaining('Betrag: 42,00 GDD,') &&
|
||||
expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') &&
|
||||
expect.stringContaining(
|
||||
'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview',
|
||||
) &&
|
||||
expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'),
|
||||
})
|
||||
})
|
||||
})
|
||||
28
backend/src/mailer/sendTransactionLinkRedeemed.ts
Normal file
28
backend/src/mailer/sendTransactionLinkRedeemed.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { sendEMail } from './sendEMail'
|
||||
import { transactionLinkRedeemed } from './text/transactionLinkRedeemed'
|
||||
|
||||
export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
email: string
|
||||
senderFirstName: string
|
||||
senderLastName: string
|
||||
recipientFirstName: string
|
||||
recipientLastName: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
logger.info(
|
||||
`sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName},
|
||||
<${data.email}>,
|
||||
subject=${transactionLinkRedeemed.de.subject},
|
||||
text=${transactionLinkRedeemed.de.text(data)}`,
|
||||
)
|
||||
return sendEMail({
|
||||
to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`,
|
||||
subject: transactionLinkRedeemed.de.subject,
|
||||
text: transactionLinkRedeemed.de.text(data),
|
||||
})
|
||||
}
|
||||
@ -19,7 +19,6 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
email: 'peter@lustig.de',
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
amount: new Decimal(42.0),
|
||||
memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!',
|
||||
overviewURL: 'http://localhost/overview',
|
||||
})
|
||||
})
|
||||
@ -33,7 +32,6 @@ describe('sendTransactionReceivedEmail', () => {
|
||||
expect.stringContaining('42,00 GDD') &&
|
||||
expect.stringContaining('Bibi Bloxberg') &&
|
||||
expect.stringContaining('(bibi@bloxberg.de)') &&
|
||||
expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') &&
|
||||
expect.stringContaining('http://localhost/overview'),
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,7 +11,6 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
email: string
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): Promise<boolean> => {
|
||||
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
|
||||
senderEmail: string
|
||||
amount: Decimal
|
||||
memo: string
|
||||
overviewURL: string
|
||||
}): string =>
|
||||
`Hallo ${data.recipientFirstName} ${data.recipientLastName}
|
||||
@ -19,16 +18,12 @@ export const transactionReceived = {
|
||||
Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
|
||||
data.senderLastName
|
||||
} (${data.senderEmail}) erhalten.
|
||||
${data.senderFirstName} ${data.senderLastName} schreibt:
|
||||
|
||||
${data.memo}
|
||||
Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
|
||||
|
||||
Bitte antworte nicht auf diese E-Mail!
|
||||
|
||||
Mit freundlichen Grüßen,
|
||||
dein Gradido-Team
|
||||
|
||||
|
||||
Link zu deinem Konto: ${data.overviewURL}`,
|
||||
dein Gradido-Team`,
|
||||
},
|
||||
}
|
||||
|
||||
@ -11,7 +11,11 @@ export const contributionLinkFactory = async (
|
||||
const { mutate, query } = client
|
||||
|
||||
// login as admin
|
||||
await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const user = await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
|
||||
const variables = {
|
||||
amount: contributionLink.amount,
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { login } from '@/seeds/graphql/queries'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { User } from '@entity/User'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Contribution } from '@entity/Contribution'
|
||||
import { findUserByEmail } from '@/graphql/resolver/UserResolver'
|
||||
// import CONFIG from '@/config/index'
|
||||
|
||||
export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
@ -19,29 +20,41 @@ export const creationFactory = async (
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution | void> => {
|
||||
const { mutate, query } = client
|
||||
|
||||
logger.trace('creationFactory...')
|
||||
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
|
||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||
logger.trace('creationFactory... after adminCreateContribution')
|
||||
|
||||
const user = await User.findOneOrFail({ where: { email: creation.email } })
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
|
||||
const pendingCreation = await Contribution.findOneOrFail({
|
||||
where: { userId: user.id, amount: creation.amount },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail pendingCreation=',
|
||||
pendingCreation,
|
||||
)
|
||||
if (creation.confirmed) {
|
||||
logger.trace('creationFactory... creation.confirmed=', creation.confirmed)
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
|
||||
logger.trace('creationFactory... after confirmContribution')
|
||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate)
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction)
|
||||
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedCreation.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
@ -52,11 +65,17 @@ export const creationFactory = async (
|
||||
transaction.balanceDate = new Date(
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
logger.trace('creationFactory... before transaction.save transaction=', transaction)
|
||||
await transaction.save()
|
||||
logger.trace(
|
||||
'creationFactory... before confirmedCreation.save confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
await confirmedCreation.save()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.trace('creationFactory... pendingCreation=', pendingCreation)
|
||||
return pendingCreation
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { createUser, setPassword } from '@/seeds/graphql/mutations'
|
||||
import { User } from '@entity/User'
|
||||
import { LoginEmailOptIn } from '@entity/LoginEmailOptIn'
|
||||
import { UserInterface } from '@/seeds/users/UserInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
|
||||
@ -15,17 +14,23 @@ export const userFactory = async (
|
||||
createUser: { id },
|
||||
},
|
||||
} = await mutate({ mutation: createUser, variables: user })
|
||||
// console.log('creatUser:', { id }, { user })
|
||||
// get user from database
|
||||
let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact'] })
|
||||
// console.log('dbUser:', dbUser)
|
||||
|
||||
const emailContact = dbUser.emailContact
|
||||
// console.log('emailContact:', emailContact)
|
||||
|
||||
if (user.emailChecked) {
|
||||
const optin = await LoginEmailOptIn.findOneOrFail({ userId: id })
|
||||
await mutate({
|
||||
mutation: setPassword,
|
||||
variables: { password: 'Aa12345_', code: optin.verificationCode },
|
||||
variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode },
|
||||
})
|
||||
}
|
||||
|
||||
// get user from database
|
||||
const dbUser = await User.findOneOrFail({ id })
|
||||
// get last changes of user from database
|
||||
dbUser = await User.findOneOrFail({ id })
|
||||
|
||||
if (user.createdAt || user.deletedAt || user.isAdmin) {
|
||||
if (user.createdAt) dbUser.createdAt = user.createdAt
|
||||
@ -34,5 +39,8 @@ export const userFactory = async (
|
||||
await dbUser.save()
|
||||
}
|
||||
|
||||
// get last changes of user from database
|
||||
// dbUser = await User.findOneOrFail({ id }, { withDeleted: true })
|
||||
|
||||
return dbUser
|
||||
}
|
||||
|
||||
@ -261,3 +261,31 @@ export const deleteContribution = gql`
|
||||
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/explicit-module-boundary-types */
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import createServer from '../server/createServer'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
@ -50,11 +51,14 @@ const run = async () => {
|
||||
const seedClient = createTestClient(server.apollo)
|
||||
const { con } = server
|
||||
await cleanDB()
|
||||
logger.info('##seed## clean database successful...')
|
||||
|
||||
// seed the standard users
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
await userFactory(seedClient, users[i])
|
||||
const dbUser = await userFactory(seedClient, users[i])
|
||||
logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`)
|
||||
}
|
||||
logger.info('##seed## seeding all standard users successful...')
|
||||
|
||||
// seed 100 random users
|
||||
for (let i = 0; i < 100; i++) {
|
||||
@ -64,7 +68,9 @@ const run = async () => {
|
||||
email: internet.email(),
|
||||
language: datatype.boolean() ? 'en' : 'de',
|
||||
})
|
||||
logger.info(`##seed## seed ${i}. random user`)
|
||||
}
|
||||
logger.info('##seed## seeding all random users successful...')
|
||||
|
||||
// create GDD
|
||||
for (let i = 0; i < creations.length; i++) {
|
||||
@ -73,16 +79,19 @@ const run = async () => {
|
||||
// eslint-disable-next-line no-empty
|
||||
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
||||
}
|
||||
logger.info('##seed## seeding all creations successful...')
|
||||
|
||||
// create Transaction Links
|
||||
for (let i = 0; i < transactionLinks.length; i++) {
|
||||
await transactionLinkFactory(seedClient, transactionLinks[i])
|
||||
}
|
||||
logger.info('##seed## seeding all transactionLinks successful...')
|
||||
|
||||
// create Contribution Links
|
||||
for (let i = 0; i < contributionLinks.length; i++) {
|
||||
await contributionLinkFactory(seedClient, contributionLinks[i])
|
||||
}
|
||||
logger.info('##seed## seeding all contributionLinks successful...')
|
||||
|
||||
await con.close()
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ export interface Context {
|
||||
setHeaders: { key: string; value: string }[]
|
||||
role?: Role
|
||||
user?: dbUser
|
||||
clientRequestTime?: string
|
||||
// hack to use less DB calls for Balance Resolver
|
||||
lastTransaction?: dbTransaction
|
||||
transactionCount?: number
|
||||
@ -18,14 +19,17 @@ export interface Context {
|
||||
|
||||
const context = (args: ExpressContext): Context => {
|
||||
const authorization = args.req.headers.authorization
|
||||
let token: string | null = null
|
||||
if (authorization) {
|
||||
token = authorization.replace(/^Bearer /, '')
|
||||
}
|
||||
const context = {
|
||||
token,
|
||||
const clientRequestTime = args.req.headers.clientrequesttime
|
||||
const context: Context = {
|
||||
token: null,
|
||||
setHeaders: [],
|
||||
}
|
||||
if (authorization) {
|
||||
context.token = authorization.replace(/^Bearer /, '')
|
||||
}
|
||||
if (clientRequestTime && typeof clientRequestTime === 'string') {
|
||||
context.clientRequestTime = clientRequestTime
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
|
||||
@ -75,6 +75,9 @@ const createServer = async (
|
||||
logger,
|
||||
})
|
||||
apollo.applyMiddleware({ app, path: '/' })
|
||||
logger.info(
|
||||
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
|
||||
)
|
||||
logger.debug('createServer...successful')
|
||||
return { apollo, app, con }
|
||||
}
|
||||
|
||||
@ -1,28 +1,39 @@
|
||||
import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm'
|
||||
import { User } from '@entity/User'
|
||||
import SearchUsersFilters from '@/graphql/arg/SearchUsersFilters'
|
||||
import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm'
|
||||
import { User as DbUser } from '@entity/User'
|
||||
|
||||
@EntityRepository(User)
|
||||
export class UserRepository extends Repository<User> {
|
||||
async findByPubkeyHex(pubkeyHex: string): Promise<User> {
|
||||
return this.createQueryBuilder('user')
|
||||
@EntityRepository(DbUser)
|
||||
export class UserRepository extends Repository<DbUser> {
|
||||
async findByPubkeyHex(pubkeyHex: string): Promise<DbUser> {
|
||||
const dbUser = await this.createQueryBuilder('user')
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex })
|
||||
.getOneOrFail()
|
||||
/*
|
||||
const dbUser = await this.findOneOrFail(`hex(user.pubKey) = { pubkeyHex }`)
|
||||
const emailContact = await this.query(
|
||||
`SELECT * from user_contacts where id = { dbUser.emailId }`,
|
||||
)
|
||||
dbUser.emailContact = emailContact
|
||||
*/
|
||||
return dbUser
|
||||
}
|
||||
|
||||
async findBySearchCriteriaPagedFiltered(
|
||||
select: string[],
|
||||
searchCriteria: string,
|
||||
filterCriteria: ObjectLiteral[],
|
||||
filters: SearchUsersFilters,
|
||||
currentPage: number,
|
||||
pageSize: number,
|
||||
): Promise<[User[], number]> {
|
||||
const query = await this.createQueryBuilder('user')
|
||||
): Promise<[DbUser[], number]> {
|
||||
const query = this.createQueryBuilder('user')
|
||||
.select(select)
|
||||
.leftJoinAndSelect('user.emailContact', 'emailContact')
|
||||
.withDeleted()
|
||||
.where(
|
||||
new Brackets((qb) => {
|
||||
qb.where(
|
||||
'user.firstName like :name or user.lastName like :lastName or user.email like :email',
|
||||
'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email',
|
||||
{
|
||||
name: `%${searchCriteria}%`,
|
||||
lastName: `%${searchCriteria}%`,
|
||||
@ -31,9 +42,23 @@ export class UserRepository extends Repository<User> {
|
||||
)
|
||||
}),
|
||||
)
|
||||
/*
|
||||
filterCriteria.forEach((filter) => {
|
||||
query.andWhere(filter)
|
||||
})
|
||||
*/
|
||||
if (filters) {
|
||||
if (filters.byActivated !== null) {
|
||||
query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated })
|
||||
// filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated })
|
||||
}
|
||||
|
||||
if (filters.byDeleted !== null) {
|
||||
// filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||
query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() })
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
.take(pageSize)
|
||||
.skip((currentPage - 1) * pageSize)
|
||||
|
||||
@ -2,22 +2,26 @@
|
||||
|
||||
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { UserContact } from '@entity/UserContact'
|
||||
// import { UserContact as EmailContact } from '@entity/UserContact'
|
||||
import { User } from '@model/User'
|
||||
|
||||
const communityDbUser: dbUser = {
|
||||
id: -1,
|
||||
gradidoID: '11111111-2222-4333-4444-55555555',
|
||||
alias: '',
|
||||
email: 'support@gradido.net',
|
||||
// email: 'support@gradido.net',
|
||||
emailContact: new UserContact(),
|
||||
emailId: -1,
|
||||
firstName: 'Gradido',
|
||||
lastName: 'Akademie',
|
||||
pubKey: Buffer.from(''),
|
||||
privKey: Buffer.from(''),
|
||||
deletedAt: null,
|
||||
password: BigInt(0),
|
||||
emailHash: Buffer.from(''),
|
||||
// emailHash: Buffer.from(''),
|
||||
createdAt: new Date(),
|
||||
emailChecked: false,
|
||||
// emailChecked: false,
|
||||
language: '',
|
||||
isAdmin: null,
|
||||
publisherId: 0,
|
||||
|
||||
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 { UserResolver } from '@/graphql/resolver/UserResolver'
|
||||
import { User as dbUser } from '@entity/User'
|
||||
import { UserContact as dbUserContact } from '@entity/UserContact'
|
||||
|
||||
export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -127,7 +127,8 @@ export const elopageWebhook = async (req: any, res: any): Promise<void> => {
|
||||
}
|
||||
|
||||
// Do we already have such a user?
|
||||
if ((await dbUser.count({ email })) !== 0) {
|
||||
// if ((await dbUser.count({ email })) !== 0) {
|
||||
if ((await dbUserContact.count({ email })) !== 0) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Did not create User - already exists with email: ${email}`)
|
||||
return
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm'
|
||||
import { Contribution } from '../Contribution'
|
||||
import { User } from '../User'
|
||||
|
||||
@Entity('contribution_messages', {
|
||||
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 })
|
||||
userId: number
|
||||
|
||||
@ManyToOne(() => User, (user) => user.messages)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User
|
||||
|
||||
@Column({ length: 2000, nullable: false, collation: 'utf8mb4_unicode_ci' })
|
||||
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 { TransactionLink } from './TransactionLink'
|
||||
import { User } from './User'
|
||||
import { UserContact } from './UserContact'
|
||||
import { Contribution } from './Contribution'
|
||||
import { EventProtocol } from './EventProtocol'
|
||||
import { ContributionMessage } from './ContributionMessage'
|
||||
@ -20,4 +21,5 @@ export const entities = [
|
||||
User,
|
||||
EventProtocol,
|
||||
ContributionMessage,
|
||||
UserContact,
|
||||
]
|
||||
|
||||
@ -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",
|
||||
"version": "1.11.0",
|
||||
"version": "1.12.1",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"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"
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v9.2022-07-07
|
||||
BACKEND_CONFIG_VERSION=v10.2022-09-20
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
ENV_NAME=stage1
|
||||
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||
|
||||
@ -40,6 +41,8 @@ KLICKTIPP_APIKEY_DE=
|
||||
KLICKTIPP_APIKEY_EN=
|
||||
|
||||
EMAIL=true
|
||||
EMAIL_TEST_MODUS=false
|
||||
EMAIL_TEST_RECEIVER=test_team@gradido.net
|
||||
EMAIL_USERNAME=peter@lustig.de
|
||||
EMAIL_SENDER=peter@lustig.de
|
||||
EMAIL_PASSWORD=1234
|
||||
@ -61,7 +64,7 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
# frontend
|
||||
FRONTEND_CONFIG_VERSION=v2.2022-04-07
|
||||
FRONTEND_CONFIG_VERSION=v3.2022-09-16
|
||||
|
||||
GRAPHQL_URI=https://stage1.gradido.net/graphql
|
||||
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_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# admin
|
||||
ADMIN_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
|
||||
@ -4,6 +4,12 @@
|
||||
# How to do this is described in detail in [setup.md](./setup.md)
|
||||
|
||||
# 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
|
||||
SCRIPT_PATH=$(realpath $0)
|
||||
SCRIPT_DIR=$(dirname $SCRIPT_PATH)
|
||||
@ -75,7 +81,7 @@ pm2 startup
|
||||
sudo apt-get install -y certbot
|
||||
sudo apt-get install -y python3-certbot-nginx
|
||||
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
|
||||
> 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
|
||||
@ -90,7 +96,7 @@ sudo certbot
|
||||
# Install 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
|
||||
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
|
||||
|
||||
# Install mysql autobackup
|
||||
@ -131,6 +137,10 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env
|
||||
# Configure admin
|
||||
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
|
||||
# Note: on first startup some errors will occur - nothing serious
|
||||
./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