Merge branch 'master' into 2592-Gradido-Logo-blurred

This commit is contained in:
Alexander Friedland 2023-02-16 14:30:49 +01:00 committed by GitHub
commit 09c75bc204
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 2577 additions and 2484 deletions

View File

@ -360,6 +360,25 @@ jobs:
- name: backend | Lint - name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint run: docker run --rm gradido/backend:test yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
##############################################################################
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES BACKEND #####################################################
##########################################################################
- name: Backend | Locales
run: cd backend && yarn && yarn locales
############################################################################## ##############################################################################
# JOB: LINT DATABASE UP ###################################################### # JOB: LINT DATABASE UP ######################################################
############################################################################## ##############################################################################
@ -478,7 +497,7 @@ jobs:
report_name: Coverage Admin Interface report_name: Coverage Admin Interface
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 96 min_coverage: 97
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################
@ -526,7 +545,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 78 min_coverage: 80
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################

View File

@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.18.2](https://github.com/gradido/gradido/compare/1.18.1...1.18.2)
- fix(admin): deny contribution button to left [`#2699`](https://github.com/gradido/gradido/pull/2699)
#### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1) #### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1)
> 10 February 2023
- chore(release): version 1.18.1 [`#2698`](https://github.com/gradido/gradido/pull/2698)
- fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697) - fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697)
- fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696) - fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696)

View File

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

View File

@ -42,14 +42,73 @@ describe('ContributionLink', () => {
expect(wrapper.find('div.contribution-link').exists()).toBe(true) expect(wrapper.find('div.contribution-link').exists()).toBe(true)
}) })
it('emits toggle::collapse new Contribution', async () => { it('has one contribution link in table', () => {
wrapper.vm.editContributionLinkData() expect(wrapper.find('div.contribution-link-list').find('tbody').findAll('tr')).toHaveLength(1)
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
}) })
it('emits toggle::collapse close Contribution-Form ', async () => { it('has contribution form not visible by default', () => {
wrapper.vm.closeContributionForm() expect(wrapper.find('#newContribution').isVisible()).toBe(false)
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy() })
describe('click on create new contribution', () => {
beforeEach(async () => {
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
})
it('shows the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(true)
})
describe('click on create new contribution again', () => {
beforeEach(async () => {
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
describe('click on close button', () => {
beforeEach(async () => {
await wrapper.find('button.btn-secondary').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
})
describe('edit contribution link', () => {
beforeEach(async () => {
await wrapper
.find('div.contribution-link-list')
.find('tbody')
.findAll('tr')
.at(0)
.findAll('button')
.at(1)
.trigger('click')
})
it('shows the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(true)
})
it('does not show the new contribution button', () => {
expect(wrapper.find('[data-test="new-contribution-link-button"]').exists()).toBe(false)
})
describe('click on close button', () => {
beforeEach(async () => {
await wrapper.find('button.btn-secondary').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
}) })
}) })
}) })

View File

@ -10,8 +10,9 @@
> >
<b-button <b-button
v-if="!editContributionLink" v-if="!editContributionLink"
v-b-toggle.newContribution @click="visible = !visible"
class="my-3 d-flex justify-content-left" class="my-3 d-flex justify-content-left"
data-test="new-contribution-link-button"
> >
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }} {{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
</b-button> </b-button>

View File

@ -70,8 +70,6 @@ export default {
formatter: (value, key, item) => { formatter: (value, key, item) => {
if (value) { if (value) {
return this.$d(new Date(value)) return this.$d(new Date(value))
} else {
return null
} }
}, },
}, },
@ -81,8 +79,6 @@ export default {
formatter: (value, key, item) => { formatter: (value, key, item) => {
if (value) { if (value) {
return this.$d(new Date(value)) return this.$d(new Date(value))
} else {
return null
} }
}, },
}, },

View File

@ -88,5 +88,16 @@ describe('CreationTransactionList', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!') expect(toastErrorSpy).toBeCalledWith('OUCH!')
}) })
}) })
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
})
})
}) })
}) })

View File

@ -46,43 +46,45 @@ describe('NavBar', () => {
}) })
describe('Navbar Menu', () => { describe('Navbar Menu', () => {
it('has a link to overview', () => {
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/')
})
it('has a link to /user', () => { it('has a link to /user', () => {
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user') expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/user')
})
it('has a link to /creation', () => {
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation')
}) })
it('has a link to /creation-confirm', () => { it('has a link to /creation-confirm', () => {
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe( expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe(
'/creation-confirm', '/creation-confirm',
) )
}) })
it('has a link to /contribution-links', () => { it('has a link to /contribution-links', () => {
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe( expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe(
'/contribution-links', '/contribution-links',
) )
}) })
it('has a link to /statistic', () => { it('has a link to /statistic', () => {
expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('/statistic') expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe('/statistic')
}) })
}) })
describe('wallet', () => { describe('wallet', () => {
const assignLocationSpy = jest.fn() const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => { beforeEach(async () => {
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click') delete window.location
window.location = {
assign: windowLocationMock,
}
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
afterEach(() => {
delete window.location
window.location = windowLocation
}) })
it.skip('changes window location to wallet', () => { it.skip('changes window location to wallet', () => {
expect(assignLocationSpy).toBeCalledWith('valid-token') expect(windowLocationMock()).toBe('valid-token')
}) })
it('dispatches logout to store', () => { it('dispatches logout to store', () => {
@ -92,12 +94,18 @@ describe('NavBar', () => {
describe('logout', () => { describe('logout', () => {
const windowLocationMock = jest.fn() const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => { beforeEach(async () => {
delete window.location delete window.location
window.location = { window.location = {
assign: windowLocationMock, assign: windowLocationMock,
} }
await wrapper.findAll('.nav-item').at(7).find('a').trigger('click') await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
afterEach(() => {
delete window.location
window.location = windowLocation
}) })
it('redirects to /logout', () => { it('redirects to /logout', () => {

View File

@ -9,15 +9,12 @@
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/">{{ $t('navbar.overview') }}</b-nav-item>
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item> <b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item to="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item> <b-nav-item class="bg-color-creation p-1" to="/creation-confirm">
<b-nav-item {{ $t('creation') }}
v-show="$store.state.openCreations > 0" <b-badge v-show="$store.state.openCreations > 0" variant="danger">
class="bg-color-creation p-1" {{ $store.state.openCreations }}
to="/creation-confirm" </b-badge>
>
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
</b-nav-item> </b-nav-item>
<b-nav-item to="/contribution-links"> <b-nav-item to="/contribution-links">
{{ $t('navbar.automaticContributions') }} {{ $t('navbar.automaticContributions') }}
@ -57,7 +54,4 @@ export default {
height: 2rem; height: 2rem;
padding-left: 10px; padding-left: 10px;
} }
.bg-color-creation {
background-color: #cf1010dc;
}
</style> </style>

View File

@ -13,7 +13,8 @@
<b-row> <b-row>
<b-col class="col-3">{{ $t('creation_for_month') }}</b-col> <b-col class="col-3">{{ $t('creation_for_month') }}</b-col>
<b-col class="h3"> <b-col class="h3">
{{ $d(new Date(item.date), 'month') }} {{ $d(new Date(item.date), 'year') }} {{ $d(new Date(item.contributionDate), 'month') }}
{{ $d(new Date(item.contributionDate), 'year') }}
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row>

View File

@ -1,6 +1,17 @@
<template> <template>
<div class="open-creations-table"> <div class="open-creations-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md"> <b-table-lite
:items="items"
:fields="fields"
caption-top
striped
hover
stacked="md"
:tbody-tr-class="rowClass"
>
<template #cell(state)="row">
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
</template>
<template #cell(bookmark)="row"> <template #cell(bookmark)="row">
<b-button <b-button
variant="danger" variant="danger"
@ -37,6 +48,16 @@
</b-button> </b-button>
</div> </div>
</template> </template>
<template #cell(reActive)>
<b-button variant="warning" size="md" class="mr-2">
<b-icon icon="arrow-up" variant="light"></b-icon>
</b-button>
</template>
<template #cell(chatCreation)="row">
<b-button v-if="row.item.messagesCount > 0" @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
</b-button>
</template>
<template #cell(deny)="row"> <template #cell(deny)="row">
<div v-if="$store.state.moderator.id !== row.item.userId"> <div v-if="$store.state.moderator.id !== row.item.userId">
<b-button <b-button
@ -100,6 +121,14 @@ import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue' import EditCreationFormular from '../EditCreationFormular.vue'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue' import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
const iconMap = {
IN_PROGRESS: 'question-square',
PENDING: 'bell-fill',
CONFIRMED: 'check',
DELETED: 'trash',
DENIED: 'x-circle',
}
export default { export default {
name: 'OpenCreationsTable', name: 'OpenCreationsTable',
mixins: [toggleRowDetails], mixins: [toggleRowDetails],
@ -129,6 +158,14 @@ export default {
} }
}, },
methods: { methods: {
getStatusIcon(status) {
return iconMap[status] ? iconMap[status] : 'default-icon'
},
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.state === 'CONFIRMED') return 'table-success'
if (item.state === 'DENIED') return 'table-info'
},
updateCreationData(data) { updateCreationData(data) {
const row = data.row const row = data.row
this.$emit('update-contributions', data) this.$emit('update-contributions', data)

View File

@ -1,35 +0,0 @@
<template>
<div class="component-select-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<div>
<b-button
v-if="row.item.emailChecked"
variant="warning"
size="md"
@click="$emit('push-item', row.item)"
class="mr-2"
>
<b-icon icon="plus" variant="success"></b-icon>
</b-button>
<div v-else>{{ $t('e_mail') }}{{ $t('math.exclaim') }}</div>
</div>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -1,26 +0,0 @@
<template>
<div class="component-selected-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<b-button variant="danger" size="md" @click="$emit('remove-item', row.item)" class="mr-2">
<b-icon icon="x" variant="light"></b-icon>
</b-button>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectedUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -0,0 +1,34 @@
import gql from 'graphql-tag'
export const listAllContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
) {
listAllContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
) {
contributionCount
contributionList {
id
firstName
lastName
amount
memo
createdAt
contributionDate
confirmedAt
confirmedBy
state
messagesCount
deniedAt
deniedBy
}
}
}
`

View File

@ -1,20 +0,0 @@
import gql from 'graphql-tag'
export const listUnconfirmedContributions = gql`
query {
listUnconfirmedContributions {
id
firstName
lastName
userId
email
amount
memo
date
moderator
creation
state
messageCount
}
}
`

View File

@ -1,6 +1,7 @@
{ {
"all_emails": "Alle Nutzer", "all_emails": "Alle Nutzer",
"back": "zurück", "back": "zurück",
"chat": "Chat",
"contributionLink": { "contributionLink": {
"amount": "Betrag", "amount": "Betrag",
"changeSaved": "Änderungen gespeichert", "changeSaved": "Änderungen gespeichert",
@ -29,10 +30,18 @@
"validFrom": "Startdatum", "validFrom": "Startdatum",
"validTo": "Enddatum" "validTo": "Enddatum"
}, },
"contributions": {
"all": "Alle",
"confirms": "Bestätigt",
"deleted": "Gelöscht",
"denied": "Abgelehnt",
"open": "Offen"
},
"created": "Geschöpft",
"createdAt": "Angelegt",
"creation": "Schöpfung", "creation": "Schöpfung",
"creationList": "Schöpfungsliste", "creationList": "Schöpfungsliste",
"creation_form": { "creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für", "creation_for": "Aktives Grundeinkommen für",
"enter_text": "Text eintragen", "enter_text": "Text eintragen",
"form": "Schöpfungsformular", "form": "Schöpfungsformular",
@ -49,7 +58,6 @@
"update_creation": "Schöpfung aktualisieren" "update_creation": "Schöpfung aktualisieren"
}, },
"creation_for_month": "Schöpfung für Monat", "creation_for_month": "Schöpfung für Monat",
"date": "Datum",
"delete": "Löschen", "delete": "Löschen",
"deleted": "gelöscht", "deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer", "deleted_user": "Alle gelöschten Nutzer",
@ -87,23 +95,19 @@
"lastname": "Nachname", "lastname": "Nachname",
"math": { "math": {
"equals": "=", "equals": "=",
"exclaim": "!",
"pipe": "|", "pipe": "|",
"plus": "+" "plus": "+"
}, },
"message": { "message": {
"request": "Die Anfrage wurde gesendet." "request": "Die Anfrage wurde gesendet."
}, },
"mod": "Mod",
"moderator": "Moderator", "moderator": "Moderator",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatische Beiträge", "automaticContributions": "Automatische Beiträge",
"logout": "Abmelden", "logout": "Abmelden",
"multi_creation": "Mehrfachschöpfung",
"my-account": "Mein Konto", "my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen",
"overview": "Übersicht",
"statistic": "Statistik", "statistic": "Statistik",
"user_search": "Nutzersuche" "user_search": "Nutzersuche"
}, },
@ -132,9 +136,7 @@
} }
}, },
"redeemed": "eingelöst", "redeemed": "eingelöst",
"remove": "Entfernen",
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.", "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen",
"save": "Speichern", "save": "Speichern",
"statistic": { "statistic": {
"activeUsers": "Aktive Mitglieder", "activeUsers": "Aktive Mitglieder",

View File

@ -1,6 +1,7 @@
{ {
"all_emails": "All users", "all_emails": "All users",
"back": "back", "back": "back",
"chat": "Chat",
"contributionLink": { "contributionLink": {
"amount": "Amount", "amount": "Amount",
"changeSaved": "Changes saved", "changeSaved": "Changes saved",
@ -29,10 +30,18 @@
"validFrom": "Start-date", "validFrom": "Start-date",
"validTo": "End-Date" "validTo": "End-Date"
}, },
"contributions": {
"all": "All",
"confirms": "Confirmed",
"deleted": "Deleted",
"denied": "Denied",
"open": "Open"
},
"created": "Confirmed",
"createdAt": "Created",
"creation": "Creation", "creation": "Creation",
"creationList": "Creation list", "creationList": "Creation list",
"creation_form": { "creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for", "creation_for": "Active Basic Income for",
"enter_text": "Enter text", "enter_text": "Enter text",
"form": "Creation form", "form": "Creation form",
@ -49,7 +58,6 @@
"update_creation": "Creation update" "update_creation": "Creation update"
}, },
"creation_for_month": "Creation for month", "creation_for_month": "Creation for month",
"date": "Date",
"delete": "Delete", "delete": "Delete",
"deleted": "deleted", "deleted": "deleted",
"deleted_user": "All deleted user", "deleted_user": "All deleted user",
@ -87,23 +95,19 @@
"lastname": "Lastname", "lastname": "Lastname",
"math": { "math": {
"equals": "=", "equals": "=",
"exclaim": "!",
"pipe": "|", "pipe": "|",
"plus": "+" "plus": "+"
}, },
"message": { "message": {
"request": "Request has been sent." "request": "Request has been sent."
}, },
"mod": "Mod",
"moderator": "Moderator", "moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatic Contributions", "automaticContributions": "Automatic Contributions",
"logout": "Logout", "logout": "Logout",
"multi_creation": "Multiple creation",
"my-account": "My Account", "my-account": "My Account",
"open_creation": "Open creations",
"overview": "Overview",
"statistic": "Statistic", "statistic": "Statistic",
"user_search": "User search" "user_search": "User search"
}, },
@ -132,9 +136,7 @@
} }
}, },
"redeemed": "redeemed", "redeemed": "redeemed",
"remove": "Remove",
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.", "removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"remove_all": "Remove all users",
"save": "Speichern", "save": "Speichern",
"statistic": { "statistic": {
"activeUsers": "Active members", "activeUsers": "Active members",

View File

@ -0,0 +1,18 @@
import locales from './index.js'
describe('locales', () => {
it('should contain 2 locales', () => {
expect(locales).toHaveLength(2)
})
it('should contain a German locale', () => {
expect(locales).toContainEqual(
expect.objectContaining({
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
enabled: true,
}),
)
})
})

View File

@ -1,10 +1,11 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import ContributionLinks from './ContributionLinks.vue' import ContributionLinks from './ContributionLinks.vue'
import { listContributionLinks } from '@/graphql/listContributionLinks.js' import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValueOnce({ const apolloQueryMock = jest.fn().mockResolvedValue({
data: { data: {
listContributionLinks: { listContributionLinks: {
links: [ links: [
@ -47,6 +48,7 @@ describe('ContributionLinks', () => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
describe('apollo returns', () => {
it('calls listContributionLinks', () => { it('calls listContributionLinks', () => {
expect(apolloQueryMock).toBeCalledWith( expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -55,4 +57,22 @@ describe('ContributionLinks', () => {
) )
}) })
}) })
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith(
'listContributionLinks has no result, use default data',
)
})
})
})
}) })

View File

@ -1,337 +0,0 @@
import { mount } from '@vue/test-utils'
import Creation from './Creation.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: {
userCount: 2,
userList: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
emailChecked: true,
},
],
},
},
})
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$store: {
commit: storeCommitMock,
state: {
userSelectedInMassCreation: [],
},
},
}
describe('Creation', () => {
let wrapper
const Wrapper = () => {
return mount(Creation, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('has a DIV element with the class.creation', () => {
expect(wrapper.find('div.creation').exists()).toBeTruthy()
})
describe('apollo returns user array', () => {
it('calls the searchUser query', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
it('has two rows in the left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has nwo rows in the right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('has correct data in first row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi')
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'Bloxberg',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'200 | 400 | 600',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has correct data in second row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Benjamin',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Blümchen',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'800 | 600 | 400',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('push item', () => {
beforeEach(() => {
wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click')
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the correct user in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has the correct user in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('updates userSelectedInMassCreation in store', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
])
})
describe('remove item', () => {
beforeEach(async () => {
await wrapper
.findAll('table')
.at(1)
.findAll('tbody > tr')
.at(0)
.find('button')
.trigger('click')
})
it('has two items in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has the removed user in first row', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array as userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
})
describe('remove all bookmarks', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('button.btn-light').trigger('click')
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array to userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
it('calls searchUsers', () => {
expect(apolloQueryMock).toBeCalled()
})
})
})
describe('store has items in userSelectedInMassCreation', () => {
beforeEach(() => {
mocks.$store.state.userSelectedInMassCreation = [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
]
wrapper = Wrapper()
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the stored user in second row', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('failed creations', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'CreationFormular' })
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
})
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'bibi@bloxberg.de' },
])
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'benjamin@bluemchen.de' },
])
})
})
describe('watchers', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('search criteria', () => {
beforeEach(async () => {
await wrapper.setData({ criteria: 'XX' })
})
it('calls API when criteria changes', async () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
describe('reset search criteria', () => {
it('calls the API', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
})
})
it('calls API when currentPage changes', async () => {
await wrapper.setData({ currentPage: 2 })
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 2,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Ouch',
})
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})
})

View File

@ -1,200 +0,0 @@
<template>
<div class="creation">
<b-row>
<b-col cols="12" lg="6">
<label>{{ $t('user_search') }}</label>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="criteria"
:placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
<select-users-table
v-if="itemsList.length > 0"
:items="itemsList"
:fields="Searchfields"
@push-item="pushItem"
/>
<b-pagination
pills
v-model="currentPage"
per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
</b-col>
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
<div v-show="itemsMassCreation.length > 0">
<div class="text-right pr-4 mb-1">
<b-button @click="removeAllBookmarks()" variant="light">
<b-icon icon="x" scale="2" variant="danger"></b-icon>
{{ $t('remove_all') }}
</b-button>
</div>
<selected-users-table
class="shadow p-3 mb-5 bg-white rounded"
:items="itemsMassCreation"
:fields="fields"
@remove-item="removeItem"
/>
</div>
<div v-if="itemsMassCreation.length === 0">
{{ $t('multiple_creation_text') }}
</div>
<creation-formular
v-else
type="massCreation"
:creation="creation"
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmarks"
@toast-failed-creations="toastFailedCreations"
/>
</b-col>
</b-row>
</div>
</template>
<script>
import CreationFormular from '../components/CreationFormular.vue'
import SelectUsersTable from '../components/Tables/SelectUsersTable.vue'
import SelectedUsersTable from '../components/Tables/SelectedUsersTable.vue'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'Creation',
mixins: [creationMonths],
components: {
CreationFormular,
SelectUsersTable,
SelectedUsersTable,
},
data() {
return {
showArrays: false,
itemsList: [],
itemsMassCreation: this.$store.state.userSelectedInMassCreation,
radioSelectedMass: '',
criteria: '',
rows: 0,
currentPage: 1,
perPage: 25,
now: Date.now(),
}
},
async created() {
await this.getUsers()
},
methods: {
async getUsers() {
this.$apollo
.query({
query: searchUsers,
variables: {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
filters: {
byActivated: true,
byDeleted: false,
},
},
fetchPolicy: 'network-only',
})
.then((result) => {
this.rows = result.data.searchUsers.userCount
this.itemsList = result.data.searchUsers.userList.map((user) => {
return {
...user,
showDetails: false,
}
})
if (this.itemsMassCreation.length !== 0) {
const selectedIndices = this.itemsMassCreation.map((item) => item.userId)
this.itemsList = this.itemsList.filter((item) => !selectedIndices.includes(item.userId))
}
})
.catch((error) => {
this.toastError(error.message)
})
},
pushItem(selectedItem) {
this.itemsMassCreation = [
this.itemsList.find((item) => selectedItem.userId === item.userId),
...this.itemsMassCreation,
]
this.itemsList = this.itemsList.filter((item) => selectedItem.userId !== item.userId)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeItem(selectedItem) {
this.itemsList = [
this.itemsMassCreation.find((item) => selectedItem.userId === item.userId),
...this.itemsList,
]
this.itemsMassCreation = this.itemsMassCreation.filter(
(item) => selectedItem.userId !== item.userId,
)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeAllBookmarks() {
this.itemsMassCreation = []
this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers()
},
toastFailedCreations(failedCreations) {
failedCreations.forEach((email) =>
this.toastError(this.$t('creation_form.creation_failed', { email })),
)
},
},
computed: {
Searchfields() {
return [
{ key: 'bookmark', label: 'bookmark' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'email', label: this.$t('e_mail') },
]
},
fields() {
return [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'bookmark', label: this.$t('remove') },
]
},
},
watch: {
currentPage() {
this.getUsers()
},
criteria() {
this.getUsers()
},
},
}
</script>

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue' import CreationConfirm from './CreationConfirm.vue'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions' import { listAllContributions } from '../graphql/listAllContributions'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
@ -38,7 +38,9 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
listUnconfirmedContributions: [ listAllContributions: {
contributionCount: 2,
contributionList: [
{ {
id: 1, id: 1,
firstName: 'Bibi', firstName: 'Bibi',
@ -51,7 +53,15 @@ const defaultData = () => {
moderator: 1, moderator: 1,
state: 'PENDING', state: 'PENDING',
creation: [500, 500, 500], creation: [500, 500, 500],
messageCount: 0, messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
}, },
{ {
id: 2, id: 2,
@ -65,23 +75,31 @@ const defaultData = () => {
moderator: 1, moderator: 1,
state: 'PENDING', state: 'PENDING',
creation: [500, 500, 500], creation: [500, 500, 500],
messageCount: 0, messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
}, },
], ],
},
} }
} }
describe('CreationConfirm', () => { describe('CreationConfirm', () => {
let wrapper let wrapper
const listUnconfirmedContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn() const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn() const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn() const confirmContributionMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
listUnconfirmedContributions, listAllContributions,
listUnconfirmedContributionsMock jest
.fn()
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -117,6 +135,10 @@ describe('CreationConfirm', () => {
it('toast an error message', () => { it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!') expect(toastErrorSpy).toBeCalledWith('Ouch!')
}) })
it('has statusFilter ["IN_PROGRESS", "PENDING"]', () => {
expect(wrapper.vm.statusFilter).toEqual(['IN_PROGRESS', 'PENDING'])
})
}) })
describe('server response is succes', () => { describe('server response is succes', () => {
@ -125,17 +147,7 @@ describe('CreationConfirm', () => {
}) })
it('has two pending creations', () => { it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2) expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2)
})
})
describe('store', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
}) })
}) })
@ -259,7 +271,7 @@ describe('CreationConfirm', () => {
describe('deny creation', () => { describe('deny creation', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.findAll('tr').at(1).findAll('button').at(2).trigger('click') await wrapper.findAll('tr').at(1).findAll('button').at(1).trigger('click')
}) })
it('opens the overlay', () => { it('opens the overlay', () => {
@ -316,5 +328,94 @@ describe('CreationConfirm', () => {
}) })
}) })
}) })
describe('filter tabs', () => {
describe('click tab "confirmed"', () => {
let refetchSpy
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="confirmed"]').trigger('click')
})
it('has statusFilter set to ["CONFIRMED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['CONFIRMED'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
describe('click tab "open"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="open"]').trigger('click')
})
it('has statusFilter set to ["IN_PROGRESS", "PENDING"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['IN_PROGRESS', 'PENDING'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
describe('click tab "denied"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="denied"]').trigger('click')
})
it('has statusFilter set to ["DENIED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['DENIED'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
describe('click tab "all"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="all"]').trigger('click')
})
it('has statusFilter set to ["IN_PROGRESS", "PENDING", "CONFIRMED", "DENIED", "DELETED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
})
})
describe('update status', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
})
it.skip('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
})
})
}) })
}) })

View File

@ -1,6 +1,50 @@
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys --> <!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
<template> <template>
<div class="creation-confirm"> <div class="creation-confirm">
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
<template #title>
{{ $t('contributions.open') }}
<b-badge v-if="$store.state.openCreations > 0" variant="danger">
{{ $store.state.openCreations }}
</b-badge>
</template>
</b-tab>
<b-tab
:title="$t('contributions.confirms')"
:title-link-attributes="{ 'data-test': 'confirmed' }"
/>
<b-tab
:title="$t('contributions.denied')"
:title-link-attributes="{ 'data-test': 'denied' }"
/>
<b-tab
:title="$t('contributions.deleted')"
:title-link-attributes="{ 'data-test': 'deleted' }"
/>
<b-tab :title="$t('contributions.all')" :title-link-attributes="{ 'data-test': 'all' }" />
</b-tabs>
</div>
<open-creations-table
class="mt-4"
:items="items"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
@update-contributions="$apollo.queries.AllContributions.refetch()"
/>
<b-pagination
pills
size="lg"
v-model="currentPage"
:per-page="pageSize"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
<div v-if="overlay" id="overlay" @dblclick="overlay = false"> <div v-if="overlay" id="overlay" @dblclick="overlay = false">
<overlay :item="item" @overlay-cancel="overlay = false"> <overlay :item="item" @overlay-cancel="overlay = false">
<template #title> <template #title>
@ -24,24 +68,24 @@
</template> </template>
</overlay> </overlay>
</div> </div>
<open-creations-table
class="mt-4"
:items="pendingCreations"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateState"
@update-contributions="$apollo.queries.PendingContributions.refetch()"
/>
</div> </div>
</template> </template>
<script> <script>
import Overlay from '../components/Overlay.vue' import Overlay from '../components/Overlay.vue'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue' import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions' import { listAllContributions } from '../graphql/listAllContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
const FILTER_TAB_MAP = [
['IN_PROGRESS', 'PENDING'],
['CONFIRMED'],
['DENIED'],
['DELETED'],
['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
]
export default { export default {
name: 'CreationConfirm', name: 'CreationConfirm',
components: { components: {
@ -50,10 +94,14 @@ export default {
}, },
data() { data() {
return { return {
pendingCreations: [], tabIndex: 0,
items: [],
overlay: false, overlay: false,
item: {}, item: {},
variant: 'confirm', variant: 'confirm',
rows: 0,
currentPage: 1,
pageSize: 25,
} }
}, },
methods: { methods: {
@ -112,7 +160,7 @@ export default {
}) })
}, },
updatePendingCreations(id) { updatePendingCreations(id) {
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id) this.items = this.items.filter((obj) => obj.id !== id)
this.$store.commit('openCreationsMinus', 1) this.$store.commit('openCreationsMinus', 1)
}, },
showOverlay(item, variant) { showOverlay(item, variant) {
@ -120,15 +168,22 @@ export default {
this.item = item this.item = item
this.variant = variant this.variant = variant
}, },
updateState(id) { updateStatus(id) {
this.pendingCreations.find((obj) => obj.id === id).messagesCount++ this.items.find((obj) => obj.id === id).messagesCount++
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS' this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
},
},
watch: {
statusFilter() {
this.$apollo.queries.ListAllContributions.refetch()
}, },
}, },
computed: { computed: {
fields() { fields() {
return [ return [
[
{ key: 'bookmark', label: this.$t('delete') }, { key: 'bookmark', label: this.$t('delete') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'email', label: this.$t('e_mail') }, { key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') }, { key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') }, { key: 'lastName', label: this.$t('lastname') },
@ -141,17 +196,127 @@ export default {
}, },
{ key: 'memo', label: this.$t('text'), class: 'text-break' }, { key: 'memo', label: this.$t('text'), class: 'text-break' },
{ {
key: 'date', key: 'contributionDate',
label: this.$t('date'), label: this.$t('created'),
formatter: (value) => { formatter: (value) => {
return this.$d(new Date(value), 'short') return this.$d(new Date(value), 'short')
}, },
}, },
{ key: 'moderator', label: this.$t('moderator') }, { key: 'moderator', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('edit') }, { key: 'editCreation', label: this.$t('edit') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'confirm', label: this.$t('save') }, { key: 'confirm', label: this.$t('save') },
] ],
[
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'chatCreation', label: this.$t('chat') },
],
[
{ key: 'reActive', label: 'reActive' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'deniedAt',
label: this.$t('contributions.denied'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'deniedBy', label: this.$t('mod') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[],
[
{ key: 'state', label: 'state' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'confirmedBy', label: this.$t('mod') },
{ key: 'chatCreation', label: this.$t('chat') },
],
][this.tabIndex]
},
statusFilter() {
return FILTER_TAB_MAP[this.tabIndex]
}, },
overlayTitle() { overlayTitle() {
return `overlay.${this.variant}.title` return `overlay.${this.variant}.title`
@ -182,18 +347,21 @@ export default {
}, },
}, },
apollo: { apollo: {
PendingContributions: { ListAllContributions: {
query() { query() {
return listUnconfirmedContributions return listAllContributions
}, },
variables() { variables() {
// may be at some point we need a pagination here // may be at some point we need a pagination here
return {} return {
currentPage: this.currentPage,
pageSize: this.pageSize,
statusFilter: this.statusFilter,
}
}, },
update({ listUnconfirmedContributions }) { update({ listAllContributions }) {
this.$store.commit('resetOpenCreations') this.rows = listAllContributions.contributionCount
this.pendingCreations = listUnconfirmedContributions this.items = listAllContributions.contributionList
this.$store.commit('setOpenCreations', listUnconfirmedContributions.length)
}, },
error({ message }) { error({ message }) {
this.toastError(message) this.toastError(message)

View File

@ -1,41 +1,18 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Overview from './Overview.vue' import Overview from './Overview.vue'
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js' import { listAllContributions } from '../graphql/listAllContributions'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest localVue.use(VueApollo)
.fn()
.mockResolvedValueOnce({
data: {
listUnconfirmedContributions: [
{
pending: true,
},
{
pending: true,
},
{
pending: true,
},
],
},
})
.mockResolvedValue({
data: {
listUnconfirmedContributions: [
{
pending: true,
},
{
pending: true,
},
{
pending: true,
},
],
},
})
const storeCommitMock = jest.fn() const storeCommitMock = jest.fn()
@ -43,44 +20,114 @@ const mocks = {
$t: jest.fn((t) => t), $t: jest.fn((t) => t),
$n: jest.fn((n) => n), $n: jest.fn((n) => n),
$d: jest.fn((d) => d), $d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$store: { $store: {
commit: storeCommitMock, commit: storeCommitMock,
state: { state: {
openCreations: 2, openCreations: 1,
}, },
}, },
} }
const defaultData = () => {
return {
listAllContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
}
}
describe('Overview', () => { describe('Overview', () => {
let wrapper let wrapper
const listAllContributionsMock = jest.fn()
mockClient.setRequestHandler(
listAllContributions,
listAllContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => { const Wrapper = () => {
return mount(Overview, { localVue, mocks }) return mount(Overview, { localVue, mocks, apolloProvider })
} }
describe('mount', () => { describe('mount', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('calls listUnconfirmedContributions', () => { describe('server response for get pending creations is error', () => {
expect(apolloQueryMock).toBeCalledWith( it('toast an error message', () => {
expect.objectContaining({ expect(toastErrorSpy).toBeCalledWith('Ouch!')
query: listUnconfirmedContributions, })
}), })
)
it('calls the listAllContributions query', () => {
expect(listAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
}) })
it('commits three pending creations to store', () => { it('commits three pending creations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3) expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
}) })
describe('with open creations', () => { describe('with open creations', () => {
it('renders a link to confirm creations', () => { beforeEach(() => {
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('2') mocks.$store.state.openCreations = 2
})
it('renders a link to confirm 2 creations', () => {
expect(wrapper.find('[data-test="open-creation"]').text()).toContain('2')
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy() expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
}) })
}) })
@ -91,7 +138,7 @@ describe('Overview', () => {
}) })
it('renders a link to confirm creations', () => { it('renders a link to confirm creations', () => {
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('0') expect(wrapper.find('[data-test="open-creation"]').text()).toContain('0')
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy() expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
}) })
}) })

View File

@ -24,31 +24,40 @@
> >
<b-card-text> <b-card-text>
<b-link to="creation-confirm"> <b-link to="creation-confirm">
<h1>{{ $store.state.openCreations }}</h1> <h1 data-test="open-creation">{{ $store.state.openCreations }}</h1>
</b-link> </b-link>
</b-card-text> </b-card-text>
</b-card> </b-card>
</div> </div>
</template> </template>
<script> <script>
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js' import { listAllContributions } from '../graphql/listAllContributions'
export default { export default {
name: 'overview', name: 'overview',
methods: { data() {
getPendingCreations() { return {
this.$apollo statusFilter: ['IN_PROGRESS', 'PENDING'],
.query({ }
query: listUnconfirmedContributions, },
fetchPolicy: 'network-only', apollo: {
}) AllContributions: {
.then((result) => { query() {
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length) return listAllContributions
}) },
variables() {
// may be at some point we need a pagination here
return {
statusFilter: this.statusFilter,
}
},
update({ listAllContributions }) {
this.$store.commit('setOpenCreations', listAllContributions.contributionCount)
},
error({ message }) {
this.toastError(message)
}, },
}, },
created() {
this.getPendingCreations()
}, },
} }
</script> </script>

View File

@ -45,7 +45,7 @@ describe('router', () => {
describe('routes', () => { describe('routes', () => {
it('has nine routes defined', () => { it('has nine routes defined', () => {
expect(routes).toHaveLength(9) expect(routes).toHaveLength(8)
}) })
it('has "/overview" as default', async () => { it('has "/overview" as default', async () => {
@ -67,13 +67,6 @@ describe('router', () => {
}) })
}) })
describe('creation', () => {
it('loads the "Creation" component', async () => {
const component = await routes.find((r) => r.path === '/creation').component()
expect(component.default.name).toBe('Creation')
})
})
describe('creation-confirm', () => { describe('creation-confirm', () => {
it('loads the "CreationConfirm" component', async () => { it('loads the "CreationConfirm" component', async () => {
const component = await routes.find((r) => r.path === '/creation-confirm').component() const component = await routes.find((r) => r.path === '/creation-confirm').component()

View File

@ -19,10 +19,6 @@ const routes = [
path: '/user', path: '/user',
component: () => import('@/pages/UserSearch.vue'), component: () => import('@/pages/UserSearch.vue'),
}, },
{
path: '/creation',
component: () => import('@/pages/Creation.vue'),
},
{ {
path: '/creation-confirm', path: '/creation-confirm',
component: () => import('@/pages/CreationConfirm.vue'), component: () => import('@/pages/CreationConfirm.vue'),

View File

@ -58,8 +58,4 @@ WEBHOOK_ELOPAGE_SECRET=secret
# LOG_LEVEL=info # LOG_LEVEL=info
# Federation # Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api

View File

@ -55,6 +55,4 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# Federation # Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.18.1", "version": "1.18.2",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -15,10 +15,10 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts .", "lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts" "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts",
"locales": "scripts/sort.sh"
}, },
"dependencies": { "dependencies": {
"@hyperswarm/dht": "^6.2.0",
"apollo-server-express": "^2.25.2", "apollo-server-express": "^2.25.2",
"await-semaphore": "^0.1.3", "await-semaphore": "^0.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -30,6 +30,7 @@
"email-templates": "^10.0.1", "email-templates": "^10.0.1",
"express": "^4.17.1", "express": "^4.17.1",
"graphql": "^15.5.1", "graphql": "^15.5.1",
"graphql-request": "5.0.0",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",

25
backend/scripts/sort.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
ROOT_DIR=$(dirname "$0")/..
tmp=$(mktemp)
exit_code=0
for locale_file in $ROOT_DIR/src/locales/*.json
do
jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp"
if [ "$*" == "--fix" ]
then
mv "$tmp" $locale_file
else
if diff -q "$tmp" $locale_file > /dev/null ;
then
: # all good
else
exit_code=$?
echo "$(basename -- $locale_file) is not sorted by keys"
fi
fi
done
exit $exit_code

View File

@ -0,0 +1,13 @@
def walk(f):
. as $in
| if type == "object" then
reduce keys_unsorted[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
def keys_sort_by(f):
to_entries | sort_by(.key|f ) | from_entries;
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)

View File

@ -10,7 +10,7 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0059-add_hide_amount_to_users', DB_VERSION: '0060-update_communities_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
@ -115,14 +115,8 @@ if (
} }
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, FEDERATION_VALIDATE_COMMUNITY_TIMER:
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
FEDERATION_COMMUNITY_URL:
process.env.FEDERATION_COMMUNITY_URL === undefined
? null
: process.env.FEDERATION_COMMUNITY_URL.endsWith('/')
? process.env.FEDERATION_COMMUNITY_URL
: process.env.FEDERATION_COMMUNITY_URL + '/',
} }
const CONFIG = { const CONFIG = {

View File

@ -1,509 +1,212 @@
import decimal from 'decimal.js-light' import { EventProtocol as DbEvent } from '@entity/EventProtocol'
import Decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType' import { EventProtocolType } from './EventProtocolType'
export class EventBasic { export const Event = (
type: string type: EventProtocolType,
createdAt: Date
}
export class EventBasicUserId extends EventBasic {
userId: number
}
export class EventBasicTx extends EventBasicUserId {
transactionId: number
amount: decimal
}
export class EventBasicTxX extends EventBasicTx {
xUserId: number
xCommunityId: number
}
export class EventBasicCt extends EventBasicUserId {
contributionId: number
amount: decimal
}
export class EventBasicCtX extends EventBasicCt {
xUserId: number
xCommunityId: number
}
export class EventBasicRedeem extends EventBasicUserId {
transactionId?: number
contributionId?: number
}
export class EventBasicCtMsg extends EventBasicCt {
messageId: number
}
export class EventVisitGradido extends EventBasic {}
export class EventRegister extends EventBasicUserId {}
export class EventRedeemRegister extends EventBasicRedeem {}
export class EventVerifyRedeem extends EventBasicRedeem {}
export class EventInactiveAccount extends EventBasicUserId {}
export class EventSendConfirmationEmail extends EventBasicUserId {}
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
export class EventSendForgotPasswordEmail extends EventBasicUserId {}
export class EventSendTransactionSendEmail extends EventBasicTxX {}
export class EventSendTransactionReceiveEmail extends EventBasicTxX {}
export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {}
export class EventSendAddedContributionEmail extends EventBasicCt {}
export class EventSendContributionConfirmEmail extends EventBasicCt {}
export class EventConfirmationEmail extends EventBasicUserId {}
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
export class EventLogin extends EventBasicUserId {}
export class EventLogout extends EventBasicUserId {}
export class EventRedeemLogin extends EventBasicRedeem {}
export class EventActivateAccount extends EventBasicUserId {}
export class EventPasswordChange extends EventBasicUserId {}
export class EventTransactionSend extends EventBasicTxX {}
export class EventTransactionSendRedeem extends EventBasicTxX {}
export class EventTransactionRepeateRedeem extends EventBasicTxX {}
export class EventTransactionCreation extends EventBasicTx {}
export class EventTransactionReceive extends EventBasicTxX {}
export class EventTransactionReceiveRedeem extends EventBasicTxX {}
export class EventContributionCreate extends EventBasicCt {}
export class EventAdminContributionCreate extends EventBasicCt {}
export class EventAdminContributionDelete extends EventBasicCt {}
export class EventAdminContributionDeny extends EventBasicCt {}
export class EventAdminContributionUpdate extends EventBasicCt {}
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
export class EventContributionDelete extends EventBasicCt {}
export class EventContributionUpdate extends EventBasicCt {}
export class EventContributionConfirm extends EventBasicCtX {}
export class EventContributionDeny extends EventBasicCtX {}
export class EventContributionLinkDefine extends EventBasicCt {}
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
export class EventDeleteUser extends EventBasicUserId {}
export class EventUndeleteUser extends EventBasicUserId {}
export class EventChangeUserRole extends EventBasicUserId {}
export class EventAdminUpdateContribution extends EventBasicCt {}
export class EventAdminDeleteContribution extends EventBasicCt {}
export class EventCreateContributionLink extends EventBasicCt {}
export class EventDeleteContributionLink extends EventBasicCt {}
export class EventUpdateContributionLink extends EventBasicCt {}
export class Event {
public setEventBasic(): Event {
this.type = EventProtocolType.BASIC
this.createdAt = new Date()
return this
}
public setEventVisitGradido(): Event {
this.setEventBasic()
this.type = EventProtocolType.VISIT_GRADIDO
return this
}
public setEventRegister(ev: EventRegister): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER
return this
}
public setEventRedeemRegister(ev: EventRedeemRegister): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_REGISTER
return this
}
public setEventVerifyRedeem(ev: EventVerifyRedeem): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.VERIFY_REDEEM
return this
}
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.INACTIVE_ACCOUNT
return this
}
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL
return this
}
public setEventSendAccountMultiRegistrationEmail(
ev: EventSendAccountMultiRegistrationEmail,
): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL
return this
}
public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL
return this
}
public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL
return this
}
public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL
return this
}
public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL
return this
}
public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL
return this
}
public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL
return this
}
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CONFIRM_EMAIL
return this
}
public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER_EMAIL_KLICKTIPP
return this
}
public setEventLogin(ev: EventLogin): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGIN
return this
}
public setEventLogout(ev: EventLogout): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGOUT
return this
}
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_LOGIN
return this
}
public setEventActivateAccount(ev: EventActivateAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.ACTIVATE_ACCOUNT
return this
}
public setEventPasswordChange(ev: EventPasswordChange): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.PASSWORD_CHANGE
return this
}
public setEventTransactionSend(ev: EventTransactionSend): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_SEND
return this
}
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
return this
}
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
return this
}
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
this.setByBasicTx(ev.userId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_CREATION
return this
}
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_RECEIVE
return this
}
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
return this
}
public setEventContributionCreate(ev: EventContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_CREATE
return this
}
public setEventAdminContributionCreate(ev: EventAdminContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_CREATE
return this
}
public setEventAdminContributionDelete(ev: EventAdminContributionDelete): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DELETE
return this
}
public setEventAdminContributionDeny(ev: EventAdminContributionDeny): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DENY
return this
}
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
return this
}
public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event {
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE
return this
}
public setEventAdminCreateContributionMessage(ev: EventAdminCreateContributionMessage): Event {
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
this.type = EventProtocolType.ADMIN_CREATE_CONTRIBUTION_MESSAGE
return this
}
public setEventContributionDelete(ev: EventContributionDelete): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_DELETE
return this
}
public setEventContributionUpdate(ev: EventContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_UPDATE
return this
}
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
return this
}
public setEventContributionDeny(ev: EventContributionDeny): Event {
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.CONTRIBUTION_DENY
return this
}
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
return this
}
public setEventContributionLinkActivateRedeem(ev: EventContributionLinkActivateRedeem): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_ACTIVATE_REDEEM
return this
}
public setEventDeleteUser(ev: EventDeleteUser): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.DELETE_USER
return this
}
public setEventUndeleteUser(ev: EventUndeleteUser): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.UNDELETE_USER
return this
}
public setEventChangeUserRole(ev: EventChangeUserRole): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CHANGE_USER_ROLE
return this
}
public setEventAdminUpdateContribution(ev: EventAdminUpdateContribution): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_UPDATE_CONTRIBUTION
return this
}
public setEventAdminDeleteContribution(ev: EventAdminDeleteContribution): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_DELETE_CONTRIBUTION
return this
}
public setEventCreateContributionLink(ev: EventCreateContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CREATE_CONTRIBUTION_LINK
return this
}
public setEventDeleteContributionLink(ev: EventDeleteContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.DELETE_CONTRIBUTION_LINK
return this
}
public setEventUpdateContributionLink(ev: EventUpdateContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.UPDATE_CONTRIBUTION_LINK
return this
}
setByBasicUser(userId: number): Event {
this.setEventBasic()
this.userId = userId
return this
}
setByBasicTx(userId: number, transactionId: number, amount: decimal): Event {
this.setByBasicUser(userId)
this.transactionId = transactionId
this.amount = amount
return this
}
setByBasicTxX(
userId: number, userId: number,
xUserId: number | null = null,
xCommunityId: number | null = null,
transactionId: number | null = null,
contributionId: number | null = null,
amount: Decimal | null = null,
messageId: number | null = null,
): DbEvent => {
const event = new DbEvent()
event.type = type
event.userId = userId
event.xUserId = xUserId
event.xCommunityId = xCommunityId
event.transactionId = transactionId
event.contributionId = contributionId
event.amount = amount
event.messageId = messageId
return event
}
export const EVENT_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_DELETE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_UPDATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_CONFIRM = async (
userId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CONFIRM,
userId,
null,
null,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
userId: number,
xUserId: number,
contributionId: number,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
userId,
xUserId,
null,
null,
contributionId,
amount,
).save()
export const EVENT_TRANSACTION_SEND = async (
userId: number,
xUserId: number,
transactionId: number, transactionId: number,
amount: decimal, amount: Decimal,
xUserId: number, ): Promise<DbEvent> =>
xCommunityId: number, Event(
): Event { EventProtocolType.TRANSACTION_SEND,
this.setByBasicTx(userId, transactionId, amount) userId,
this.xUserId = xUserId xUserId,
this.xCommunityId = xCommunityId null,
transactionId,
null,
amount,
).save()
return this export const EVENT_TRANSACTION_RECEIVE = async (
}
setByBasicCt(userId: number, contributionId: number, amount: decimal): Event {
this.setByBasicUser(userId)
this.contributionId = contributionId
this.amount = amount
return this
}
setByBasicCtMsg(
userId: number, userId: number,
contributionId: number,
amount: decimal,
messageId: number,
): Event {
this.setByBasicCt(userId, contributionId, amount)
this.messageId = messageId
return this
}
setByBasicCtX(
userId: number,
contributionId: number,
amount: decimal,
xUserId: number, xUserId: number,
xCommunityId: number, transactionId: number,
): Event { amount: Decimal,
this.setByBasicCt(userId, contributionId, amount) ): Promise<DbEvent> =>
this.xUserId = xUserId Event(
this.xCommunityId = xCommunityId EventProtocolType.TRANSACTION_RECEIVE,
userId,
xUserId,
null,
transactionId,
null,
amount,
).save()
return this export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
} Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
setByBasicRedeem(userId: number, transactionId?: number, contributionId?: number): Event { export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
this.setByBasicUser(userId) userId: number,
if (transactionId) this.transactionId = transactionId ): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
if (contributionId) this.contributionId = contributionId
return this export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
} Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
id: number export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
type: string Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
createdAt: Date
userId: number /* export const EVENT_REDEEM_REGISTER = async (
xUserId?: number userId: number,
xCommunityId?: number transactionId: number | null = null,
transactionId?: number contributionId: number | null = null,
contributionId?: number ): Promise<Event> =>
amount?: decimal Event(
messageId?: number EventProtocolType.REDEEM_REGISTER,
} userId,
null,
null,
transactionId,
contributionId,
).save()
*/
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.REGISTER, userId).save()
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()

View File

@ -1,17 +0,0 @@
import { Event } from '@/event/Event'
import { backendLogger as logger } from '@/server/logger'
import { EventProtocol } from '@entity/EventProtocol'
export const writeEvent = async (event: Event): Promise<EventProtocol | null> => {
logger.info('writeEvent', event)
const dbEvent = new EventProtocol()
dbEvent.type = event.type
dbEvent.createdAt = event.createdAt
dbEvent.userId = event.userId
dbEvent.xUserId = event.xUserId || null
dbEvent.xCommunityId = event.xCommunityId || null
dbEvent.contributionId = event.contributionId || null
dbEvent.transactionId = event.transactionId || null
dbEvent.amount = event.amount || null
return dbEvent.save()
}

View File

@ -1,50 +1,50 @@
export enum EventProtocolType { export enum EventProtocolType {
BASIC = 'BASIC', // VISIT_GRADIDO = 'VISIT_GRADIDO',
VISIT_GRADIDO = 'VISIT_GRADIDO',
REGISTER = 'REGISTER', REGISTER = 'REGISTER',
REDEEM_REGISTER = 'REDEEM_REGISTER', REDEEM_REGISTER = 'REDEEM_REGISTER',
VERIFY_REDEEM = 'VERIFY_REDEEM', // VERIFY_REDEEM = 'VERIFY_REDEEM',
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', // INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL', SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL', SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
CONFIRM_EMAIL = 'CONFIRM_EMAIL', // CONFIRM_EMAIL = 'CONFIRM_EMAIL',
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP', // REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
LOGIN = 'LOGIN', LOGIN = 'LOGIN',
LOGOUT = 'LOGOUT', // LOGOUT = 'LOGOUT',
REDEEM_LOGIN = 'REDEEM_LOGIN', // REDEEM_LOGIN = 'REDEEM_LOGIN',
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT', ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL', // SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
PASSWORD_CHANGE = 'PASSWORD_CHANGE', // PASSWORD_CHANGE = 'PASSWORD_CHANGE',
SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL', // SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL', // SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
TRANSACTION_SEND = 'TRANSACTION_SEND', TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM', // TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM', // TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
TRANSACTION_CREATION = 'TRANSACTION_CREATION', // TRANSACTION_CREATION = 'TRANSACTION_CREATION',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM', // TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL', // SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL', // SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL', // SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE', CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM', CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
CONTRIBUTION_DENY = 'CONTRIBUTION_DENY', // CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE', // CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM', // CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE', CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY', ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', // USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', // ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DELETE_USER = 'DELETE_USER', // DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER', // UNDELETE_USER = 'UNDELETE_USER',
CHANGE_USER_ROLE = 'CHANGE_USER_ROLE', // CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', // ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION', // ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', // CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', // DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', // UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
} }

View File

@ -0,0 +1,34 @@
import { gql } from 'graphql-request'
import { backendLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { GraphQLGetClient } from '../GraphQLGetClient'
import LogError from '@/server/LogError'
export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(query)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -0,0 +1,34 @@
import { gql } from 'graphql-request'
import { backendLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { GraphQLGetClient } from '../GraphQLGetClient'
import LogError from '@/server/LogError'
export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(query)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -0,0 +1,35 @@
import { GraphQLClient } from 'graphql-request'
import { PatchedRequestInit } from 'graphql-request/dist/types'
export class GraphQLGetClient extends GraphQLClient {
private static instance: GraphQLGetClient
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor
private constructor(url: string, options?: PatchedRequestInit) {
super(url, options)
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(url: string): GraphQLGetClient {
if (!GraphQLGetClient.instance) {
GraphQLGetClient.instance = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
}
return GraphQLGetClient.instance
}
}

View File

@ -0,0 +1,4 @@
export enum ApiVersionType {
V1_0 = '1_0',
V1_1 = '1_1',
}

View File

@ -0,0 +1,158 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { logger } from '@test/testSetup'
import { Community as DbCommunity } from '@entity/Community'
import { testEnvironment, cleanDB } from '@test/helpers'
import { validateCommunities } from './validateCommunities'
let con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment(logger)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
// await cleanDB()
await con.close()
})
describe('validate Communities', () => {
/*
describe('start validation loop', () => {
beforeEach(async () => {
jest.clearAllMocks()
startValidateCommunities(0)
})
it('logs loop started', () => {
expect(logger.info).toBeCalledWith(
`Federation: startValidateCommunities loop with an interval of 0 ms...`,
)
})
})
*/
describe('start validation logic without loop', () => {
beforeEach(async () => {
jest.clearAllMocks()
await validateCommunities()
})
it('logs zero communities found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`)
})
describe('with one Community of api 1_0', () => {
beforeEach(async () => {
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.values(variables1)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`,
)
})
})
describe('with two Communities of api 1_0 and 1_1', () => {
beforeEach(async () => {
const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.values(variables2)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
await validateCommunities()
})
it('logs two communities found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`,
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`,
)
})
})
describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
let dbCom: DbCommunity
beforeEach(async () => {
const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.values(variables3)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
dbCom = await DbCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
})
jest.clearAllMocks()
await validateCommunities()
})
it('logs three community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`,
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`,
)
})
it('logs unsupported api for community with api 2_0 ', () => {
expect(logger.warn).toBeCalledWith(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions=1_0,1_1`,
)
})
})
})
})

View File

@ -0,0 +1,80 @@
import { Community as DbCommunity } from '@entity/Community'
import { IsNull } from '@dbTools/typeorm'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_0_requestGetPublicKey } from './client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_1_requestGetPublicKey } from './client/1_1/FederationClient'
import { backendLogger as logger } from '@/server/logger'
import { ApiVersionType } from './enum/apiVersionType'
import LogError from '@/server/LogError'
export async function startValidateCommunities(timerInterval: number): Promise<void> {
logger.info(
`Federation: startValidateCommunities loop with an interval of ${timerInterval} ms...`,
)
// TODO: replace the timer-loop by an event-based communication to verify announced foreign communities
// better to use setTimeout twice than setInterval once -> see https://javascript.info/settimeout-setinterval
setTimeout(function run() {
validateCommunities()
setTimeout(run, timerInterval)
}, timerInterval)
}
export async function validateCommunities(): Promise<void> {
const dbCommunities: DbCommunity[] = await DbCommunity.createQueryBuilder()
.where({ foreign: true, verifiedAt: IsNull() })
.orWhere('verified_at < last_announced_at')
.getMany()
logger.debug(`Federation: found ${dbCommunities.length} dbCommunities`)
dbCommunities.forEach(async function (dbCom) {
logger.debug(`Federation: dbCom: ${JSON.stringify(dbCom)}`)
const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings)
if (apiValueStrings.includes(dbCom.apiVersion)) {
logger.debug(
`Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`,
)
try {
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom)
logger.info(
`Federation: received publicKey=${pubKey} from endpoint=${dbCom.endPoint}/${dbCom.apiVersion}`,
)
if (pubKey && pubKey === dbCom.publicKey.toString('hex')) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`)
}
/*
else {
logger.warn(`Federation: received unknown publicKey -> delete dbCom with id=${dbCom.id} `)
DbCommunity.delete({ id: dbCom.id })
}
*/
} catch (err) {
if (!isLogError(err)) {
logger.error(`Error:`, err)
}
}
} else {
logger.warn(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions=${apiValueStrings}`,
)
}
})
}
function isLogError(err: unknown) {
return err instanceof LogError
}
async function invokeVersionedRequestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return v1_0_requestGetPublicKey(dbCom)
case ApiVersionType.V1_1:
return v1_1_requestGetPublicKey(dbCom)
default:
return undefined
}
}

View File

@ -42,6 +42,7 @@ import { User } from '@entity/User'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocolType } from '@/event/EventProtocolType'
import { logger, i18n as localization } from '@test/testSetup' import { logger, i18n as localization } from '@test/testSetup'
import { UserInputError } from 'apollo-server-express' import { UserInputError } from 'apollo-server-express'
import { ContributionStatus } from '../enum/ContributionStatus'
// mock account activation email to avoid console spam // mock account activation email to avoid console spam
jest.mock('@/emails/sendEmailVariants', () => { jest.mock('@/emails/sendEmailVariants', () => {
@ -127,13 +128,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('memo text is too short (5 characters minimum)')], errors: [new GraphQLError('Memo text is too short')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < 5`) expect(logger.error).toBeCalledWith('Memo text is too short', 4)
}) })
it('throws error when memo length greater than 255 chars', async () => { it('throws error when memo length greater than 255 chars', async () => {
@ -150,13 +151,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('memo text is too long (255 characters maximum)')], errors: [new GraphQLError('Memo text is too long')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > 255`) expect(logger.error).toBeCalledWith('Memo text is too long', 259)
}) })
it('throws error when creationDate not-valid', async () => { it('throws error when creationDate not-valid', async () => {
@ -243,7 +244,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the create contribution event in the database', async () => { it('stores the CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CREATE, type: EventProtocolType.CONTRIBUTION_CREATE,
@ -417,31 +418,6 @@ describe('ContributionResolver', () => {
resetToken() resetToken()
}) })
describe('wrong contribution id', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: -1,
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No contribution found to given id.')],
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('No contribution found to given id')
})
})
describe('Memo length smaller than 5 chars', () => { describe('Memo length smaller than 5 chars', () => {
it('throws error', async () => { it('throws error', async () => {
jest.clearAllMocks() jest.clearAllMocks()
@ -458,13 +434,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('memo text is too short (5 characters minimum)')], errors: [new GraphQLError('Memo text is too short')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5') expect(logger.error).toBeCalledWith('Memo text is too short', 4)
}) })
}) })
@ -484,13 +460,38 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('memo text is too long (255 characters maximum)')], errors: [new GraphQLError('Memo text is too long')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > 255') expect(logger.error).toBeCalledWith('Memo text is too long', 259)
})
})
describe('wrong contribution id', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: -1,
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Contribution not found')],
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1)
}) })
}) })
@ -516,18 +517,16 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [new GraphQLError('Can not update contribution of another user')],
new GraphQLError(
'user of the pending contribution and send user does not correspond',
),
],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'user of the pending contribution and send user does not correspond', 'Can not update contribution of another user',
expect.any(Object),
expect.any(Number),
) )
}) })
}) })
@ -548,12 +547,64 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('An admin is not allowed to update a user contribution.')], errors: [new GraphQLError('An admin is not allowed to update an user contribution')],
}), }),
) )
}) })
// TODO check that the error is logged (need to modify AdminResolver, avoid conflicts) it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'An admin is not allowed to update an user contribution',
)
})
})
describe('contribution has wrong status', () => {
beforeAll(async () => {
const contribution = await Contribution.findOneOrFail({
id: result.data.createContribution.id,
})
contribution.contributionStatus = ContributionStatus.DELETED
contribution.save()
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
const contribution = await Contribution.findOneOrFail({
id: result.data.createContribution.id,
})
contribution.contributionStatus = ContributionStatus.PENDING
contribution.save()
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: updateContribution,
variables: {
contributionId: result.data.createContribution.id,
amount: 10.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('Contribution can not be updated due to status')],
}),
)
})
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'Contribution can not be updated due to status',
ContributionStatus.DELETED,
)
})
}) })
describe('update too much so that the limit is exceeded', () => { describe('update too much so that the limit is exceeded', () => {
@ -610,16 +661,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Currently the month of the contribution cannot change.')], errors: [new GraphQLError('Month of contribution can not be changed')],
}), }),
) )
}) })
it.skip('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('Month of contribution can not be changed')
'No information for available creations with the given creationDate=',
'Invalid Date',
)
}) })
}) })
@ -648,7 +696,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the update contribution event in the database', async () => { it('stores the CONTRIBUTION_UPDATE event in the database', async () => {
bibi = await query({ bibi = await query({
query: login, query: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
@ -1153,6 +1201,7 @@ describe('ContributionResolver', () => {
describe('wrong contribution id', () => { describe('wrong contribution id', () => {
it('returns an error', async () => { it('returns an error', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: deleteContribution, mutation: deleteContribution,
@ -1162,18 +1211,19 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution not found for given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith('Contribution not found for given id') expect(logger.error).toBeCalledWith('Contribution not found', -1)
}) })
}) })
describe('other user sends a deleteContribution', () => { describe('other user sends a deleteContribution', () => {
it('returns an error', async () => { it('returns an error', async () => {
jest.clearAllMocks()
await mutate({ await mutate({
mutation: login, mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
@ -1193,7 +1243,11 @@ describe('ContributionResolver', () => {
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith('Can not delete contribution of another user') expect(logger.error).toBeCalledWith(
'Can not delete contribution of another user',
expect.any(Object),
expect.any(Number),
)
}) })
}) })
@ -1209,7 +1263,7 @@ describe('ContributionResolver', () => {
).resolves.toBeTruthy() ).resolves.toBeTruthy()
}) })
it('stores the delete contribution event in the database', async () => { it('stores the CONTRIBUTION_DELETE event in the database', async () => {
const contribution = await mutate({ const contribution = await mutate({
mutation: createContribution, mutation: createContribution,
variables: { variables: {
@ -1269,7 +1323,10 @@ describe('ContributionResolver', () => {
}) })
it('logs the error found', () => { it('logs the error found', () => {
expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted') expect(logger.error).toBeCalledWith(
'A confirmed contribution can not be deleted',
expect.objectContaining({ contributionStatus: 'CONFIRMED' }),
)
}) })
}) })
}) })
@ -1535,15 +1592,13 @@ describe('ContributionResolver', () => {
mutate({ mutation: adminCreateContribution, variables }), mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')], errors: [new GraphQLError('Could not find user')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('Could not find user', 'bibi@bloxberg.de')
'Could not find user with email: bibi@bloxberg.de',
)
}) })
}) })
@ -1563,7 +1618,7 @@ describe('ContributionResolver', () => {
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError('This user was deleted. Cannot create a contribution.'), new GraphQLError('Cannot create contribution since the user was deleted'),
], ],
}), }),
) )
@ -1571,7 +1626,12 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'This user was deleted. Cannot create a contribution.', 'Cannot create contribution since the user was deleted',
expect.objectContaining({
user: expect.objectContaining({
deletedAt: new Date('2018-03-14T09:17:52.000Z'),
}),
}),
) )
}) })
}) })
@ -1592,7 +1652,9 @@ describe('ContributionResolver', () => {
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError('Contribution could not be saved, Email is not activated'), new GraphQLError(
'Cannot create contribution since the users email is not activated',
),
], ],
}), }),
) )
@ -1600,7 +1662,8 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'Contribution could not be saved, Email is not activated', 'Cannot create contribution since the users email is not activated',
expect.objectContaining({ emailChecked: false }),
) )
}) })
}) })
@ -1619,13 +1682,13 @@ describe('ContributionResolver', () => {
mutate({ mutation: adminCreateContribution, variables }), mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)], errors: [new GraphQLError('CreationDate is invalid')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`invalid Date for creationDate=invalid-date`) expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date')
}) })
}) })
@ -1717,7 +1780,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the admin create contribution event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_CREATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE, type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
@ -1821,17 +1884,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [new GraphQLError('Could not find User')],
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('Could not find User', 'bob@baumeister.de')
'Could not find UserContact with email: bob@baumeister.de',
)
}) })
}) })
@ -1851,13 +1910,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('User was deleted (stephen@hawking.uk)')], errors: [new GraphQLError('User was deleted')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User was deleted (stephen@hawking.uk)') expect(logger.error).toBeCalledWith('User was deleted', 'stephen@hawking.uk')
}) })
}) })
@ -1877,13 +1936,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('No contribution found to given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No contribution found to given id.') expect(logger.error).toBeCalledWith('Contribution not found', -1)
}) })
}) })
@ -1907,7 +1966,7 @@ describe('ContributionResolver', () => {
expect.objectContaining({ expect.objectContaining({
errors: [ errors: [
new GraphQLError( new GraphQLError(
'user of the pending contribution and send user does not correspond', 'User of the pending contribution and send user does not correspond',
), ),
], ],
}), }),
@ -1916,7 +1975,7 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'user of the pending contribution and send user does not correspond', 'User of the pending contribution and send user does not correspond',
) )
}) })
}) })
@ -1986,7 +2045,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the admin update contribution event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
@ -2026,7 +2085,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the admin update contribution event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE, type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
@ -2111,13 +2170,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution not found for given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') expect(logger.error).toBeCalledWith('Contribution not found', -1)
}) })
}) })
@ -2170,7 +2229,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the admin delete contribution event in the database', async () => { it('stores the ADMIN_CONTRIBUTION_DELETE event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE, type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
@ -2237,13 +2296,13 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution not found to given id.')], errors: [new GraphQLError('Contribution not found')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution not found for given id: -1') expect(logger.error).toBeCalledWith('Contribution not found', -1)
}) })
}) })
@ -2312,7 +2371,7 @@ describe('ContributionResolver', () => {
) )
}) })
it('stores the contribution confirm event in the database', async () => { it('stores the CONTRIBUTION_CONFIRM event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.CONTRIBUTION_CONFIRM, type: EventProtocolType.CONTRIBUTION_CONFIRM,
@ -2344,7 +2403,7 @@ describe('ContributionResolver', () => {
}) })
}) })
it('stores the send confirmation email event in the database', async () => { it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
@ -2354,6 +2413,7 @@ describe('ContributionResolver', () => {
describe('confirm same contribution again', () => { describe('confirm same contribution again', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: confirmContribution, mutation: confirmContribution,
@ -2363,11 +2423,18 @@ describe('ContributionResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Contribution already confirmd.')], errors: [new GraphQLError('Contribution already confirmed')],
}), }),
) )
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Contribution already confirmed',
expect.any(Number),
)
})
}) })
describe('confirm two creations one after the other quickly', () => { describe('confirm two creations one after the other quickly', () => {

View File

@ -37,17 +37,15 @@ import {
} from './util/creations' } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const'
import { import {
Event, EVENT_CONTRIBUTION_CREATE,
EventContributionCreate, EVENT_CONTRIBUTION_DELETE,
EventContributionDelete, EVENT_CONTRIBUTION_UPDATE,
EventContributionUpdate, EVENT_ADMIN_CONTRIBUTION_CREATE,
EventContributionConfirm, EVENT_ADMIN_CONTRIBUTION_UPDATE,
EventAdminContributionCreate, EVENT_ADMIN_CONTRIBUTION_DELETE,
EventAdminContributionDelete, EVENT_CONTRIBUTION_CONFIRM,
EventAdminContributionDeny, EVENT_ADMIN_CONTRIBUTION_DENY,
EventAdminContributionUpdate,
} from '@/event/Event' } from '@/event/Event'
import { writeEvent } from '@/event/EventProtocolEmitter'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
@ -55,6 +53,7 @@ import {
sendContributionDeniedEmail, sendContributionDeniedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
@ -67,17 +66,12 @@ export class ContributionResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<UnconfirmedContribution> { ): Promise<UnconfirmedContribution> {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new LogError('Memo text is too short', memo.length)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) }
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
} }
const event = new Event()
const user = getUser(context) const user = getUser(context)
const creations = await getUserCreation(user.id, clientTimezoneOffset) const creations = await getUserCreation(user.id, clientTimezoneOffset)
@ -97,11 +91,7 @@ export class ContributionResolver {
logger.trace('contribution to save', contribution) logger.trace('contribution to save', contribution)
await DbContribution.save(contribution) await DbContribution.save(contribution)
const eventCreateContribution = new EventContributionCreate() await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
eventCreateContribution.userId = user.id
eventCreateContribution.amount = amount
eventCreateContribution.contributionId = contribution.id
await writeEvent(event.setEventContributionCreate(eventCreateContribution))
return new UnconfirmedContribution(contribution, user, creations) return new UnconfirmedContribution(contribution, user, creations)
} }
@ -112,20 +102,16 @@ export class ContributionResolver {
@Arg('id', () => Int) id: number, @Arg('id', () => Int) id: number,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
const event = new Event()
const user = getUser(context) const user = getUser(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
logger.error('Contribution not found for given id') throw new LogError('Contribution not found', id)
throw new Error('Contribution not found for given id.')
} }
if (contribution.userId !== user.id) { if (contribution.userId !== user.id) {
logger.error('Can not delete contribution of another user') throw new LogError('Can not delete contribution of another user', contribution, user.id)
throw new Error('Can not delete contribution of another user')
} }
if (contribution.confirmedAt) { if (contribution.confirmedAt) {
logger.error('A confirmed contribution can not be deleted') throw new LogError('A confirmed contribution can not be deleted', contribution)
throw new Error('A confirmed contribution can not be deleted')
} }
contribution.contributionStatus = ContributionStatus.DELETED contribution.contributionStatus = ContributionStatus.DELETED
@ -133,11 +119,7 @@ export class ContributionResolver {
contribution.deletedAt = new Date() contribution.deletedAt = new Date()
await contribution.save() await contribution.save()
const eventDeleteContribution = new EventContributionDelete() await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
eventDeleteContribution.userId = user.id
eventDeleteContribution.contributionId = contribution.id
eventDeleteContribution.amount = contribution.amount
await writeEvent(event.setEventContributionDelete(eventDeleteContribution))
const res = await contribution.softRemove() const res = await contribution.softRemove()
return !!res return !!res
@ -199,6 +181,7 @@ export class ContributionResolver {
.select('c') .select('c')
.from(DbContribution, 'c') .from(DbContribution, 'c')
.innerJoinAndSelect('c.user', 'u') .innerJoinAndSelect('c.user', 'u')
.leftJoinAndSelect('c.messages', 'm')
.where(where) .where(where)
.orderBy('c.createdAt', order) .orderBy('c.createdAt', order)
.limit(pageSize) .limit(pageSize)
@ -219,14 +202,11 @@ export class ContributionResolver {
@Ctx() context: Context, @Ctx() context: Context,
): Promise<UnconfirmedContribution> { ): Promise<UnconfirmedContribution> {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
}
if (memo.length < MEMO_MIN_CHARS) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new LogError('Memo text is too short', memo.length)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) }
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
} }
const user = getUser(context) const user = getUser(context)
@ -235,22 +215,22 @@ export class ContributionResolver {
where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() }, where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() },
}) })
if (!contributionToUpdate) { if (!contributionToUpdate) {
logger.error('No contribution found to given id') throw new LogError('Contribution not found', contributionId)
throw new Error('No contribution found to given id.')
} }
if (contributionToUpdate.userId !== user.id) { if (contributionToUpdate.userId !== user.id) {
logger.error('user of the pending contribution and send user does not correspond') throw new LogError(
throw new Error('user of the pending contribution and send user does not correspond') 'Can not update contribution of another user',
contributionToUpdate,
user.id,
)
} }
if ( if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
) { ) {
logger.error( throw new LogError(
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`, 'Contribution can not be updated due to status',
) contributionToUpdate.contributionStatus,
throw new Error(
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`,
) )
} }
const creationDateObj = new Date(creationDate) const creationDateObj = new Date(creationDate)
@ -258,8 +238,7 @@ export class ContributionResolver {
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
} else { } else {
logger.error('Currently the month of the contribution cannot change.') throw new LogError('Month of contribution can not be changed')
throw new Error('Currently the month of the contribution cannot change.')
} }
// all possible cases not to be true are thrown in this function // all possible cases not to be true are thrown in this function
@ -288,13 +267,7 @@ export class ContributionResolver {
contributionToUpdate.updatedAt = new Date() contributionToUpdate.updatedAt = new Date()
DbContribution.save(contributionToUpdate) DbContribution.save(contributionToUpdate)
const event = new Event() await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount)
const eventUpdateContribution = new EventContributionUpdate()
eventUpdateContribution.userId = user.id
eventUpdateContribution.contributionId = contributionId
eventUpdateContribution.amount = amount
await writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
return new UnconfirmedContribution(contributionToUpdate, user, creations) return new UnconfirmedContribution(contributionToUpdate, user, creations)
} }
@ -310,32 +283,26 @@ export class ContributionResolver {
) )
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
if (!isValidDateString(creationDate)) { if (!isValidDateString(creationDate)) {
logger.error(`invalid Date for creationDate=${creationDate}`) throw new LogError('CreationDate is invalid', creationDate)
throw new Error(`invalid Date for creationDate=${creationDate}`)
} }
const emailContact = await UserContact.findOne({ const emailContact = await UserContact.findOne({
where: { email }, where: { email },
withDeleted: true, withDeleted: true,
relations: ['user'], relations: ['user'],
}) })
if (!emailContact) { if (!emailContact || !emailContact.user) {
logger.error(`Could not find user with email: ${email}`) throw new LogError('Could not find user', email)
throw new Error(`Could not find user with email: ${email}`)
} }
if (emailContact.deletedAt) { if (emailContact.deletedAt || emailContact.user.deletedAt) {
logger.error('This emailContact was deleted. Cannot create a contribution.') throw new LogError('Cannot create contribution since the user was deleted', emailContact)
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 (!emailContact.emailChecked) { if (!emailContact.emailChecked) {
logger.error('Contribution could not be saved, Email is not activated') throw new LogError(
throw new Error('Contribution could not be saved, Email is not activated') 'Cannot create contribution since the users email is not activated',
emailContact,
)
} }
const event = new Event()
const moderator = getUser(context) const moderator = getUser(context)
logger.trace('moderator: ', moderator.id) logger.trace('moderator: ', moderator.id)
const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset) const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset)
@ -357,11 +324,7 @@ export class ContributionResolver {
await DbContribution.save(contribution) await DbContribution.save(contribution)
const eventAdminCreateContribution = new EventAdminContributionCreate() await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
eventAdminCreateContribution.userId = moderator.id
eventAdminCreateContribution.amount = amount
eventAdminCreateContribution.contributionId = contribution.id
await writeEvent(event.setEventAdminContributionCreate(eventAdminCreateContribution))
return getUserCreation(emailContact.userId, clientTimezoneOffset) return getUserCreation(emailContact.userId, clientTimezoneOffset)
} }
@ -405,18 +368,11 @@ export class ContributionResolver {
withDeleted: true, withDeleted: true,
relations: ['user'], relations: ['user'],
}) })
if (!emailContact) { if (!emailContact || !emailContact.user) {
logger.error(`Could not find UserContact with email: ${email}`) throw new LogError('Could not find User', email)
throw new Error(`Could not find UserContact with email: ${email}`)
} }
const user = emailContact.user if (emailContact.deletedAt || emailContact.user.deletedAt) {
if (!user) { throw new LogError('User was deleted', 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 moderator = getUser(context)
@ -425,28 +381,25 @@ export class ContributionResolver {
where: { id, confirmedAt: IsNull(), deniedAt: IsNull() }, where: { id, confirmedAt: IsNull(), deniedAt: IsNull() },
}) })
if (!contributionToUpdate) { if (!contributionToUpdate) {
logger.error('No contribution found to given id.') throw new LogError('Contribution not found', id)
throw new Error('No contribution found to given id.')
} }
if (contributionToUpdate.userId !== user.id) { if (contributionToUpdate.userId !== emailContact.user.id) {
logger.error('user of the pending contribution and send user does not correspond') throw new LogError('User of the pending contribution and send user does not correspond')
throw new Error('user of the pending contribution and send user does not correspond')
} }
if (contributionToUpdate.moderatorId === null) { if (contributionToUpdate.moderatorId === null) {
logger.error('An admin is not allowed to update a user contribution.') throw new LogError('An admin is not allowed to update an user contribution')
throw new Error('An admin is not allowed to update a user contribution.')
} }
const creationDateObj = new Date(creationDate) const creationDateObj = new Date(creationDate)
let creations = await getUserCreation(user.id, clientTimezoneOffset) let creations = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
// TODO: remove this restriction
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) { if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset) creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
} else { } else {
logger.error('Currently the month of the contribution cannot change.') throw new LogError('Month of contribution can not be changed')
throw new Error('Currently the month of the contribution cannot change.')
} }
// all possible cases not to be true are thrown in this function // all possible cases not to be true are thrown in this function
@ -464,14 +417,9 @@ export class ContributionResolver {
result.memo = contributionToUpdate.memo result.memo = contributionToUpdate.memo
result.date = contributionToUpdate.contributionDate result.date = contributionToUpdate.contributionDate
result.creation = await getUserCreation(user.id, clientTimezoneOffset) result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount)
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
eventAdminContributionUpdate.userId = user.id
eventAdminContributionUpdate.amount = amount
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate))
return result return result
} }
@ -521,19 +469,17 @@ export class ContributionResolver {
): Promise<boolean> { ): Promise<boolean> {
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`) throw new LogError('Contribution not found', id)
throw new Error('Contribution not found for given id.')
} }
if (contribution.confirmedAt) { if (contribution.confirmedAt) {
logger.error('A confirmed contribution can not be deleted') throw new LogError('A confirmed contribution can not be deleted')
throw new Error('A confirmed contribution can not be deleted')
} }
const moderator = getUser(context) const moderator = getUser(context)
if ( if (
contribution.contributionType === ContributionType.USER && contribution.contributionType === ContributionType.USER &&
contribution.userId === moderator.id contribution.userId === moderator.id
) { ) {
throw new Error('Own contribution can not be deleted as admin') throw new LogError('Own contribution can not be deleted as admin')
} }
const user = await DbUser.findOneOrFail( const user = await DbUser.findOneOrFail(
{ id: contribution.userId }, { id: contribution.userId },
@ -544,12 +490,8 @@ export class ContributionResolver {
await contribution.save() await contribution.save()
const res = await contribution.softRemove() const res = await contribution.softRemove()
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount)
const eventAdminContributionDelete = new EventAdminContributionDelete()
eventAdminContributionDelete.userId = contribution.userId
eventAdminContributionDelete.amount = contribution.amount
eventAdminContributionDelete.contributionId = contribution.id
await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete))
sendContributionDeletedEmail({ sendContributionDeletedEmail({
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
@ -575,29 +517,24 @@ export class ContributionResolver {
const clientTimezoneOffset = getClientTimezoneOffset(context) const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`) throw new LogError('Contribution not found', id)
throw new Error('Contribution not found to given id.')
} }
if (contribution.confirmedAt) { if (contribution.confirmedAt) {
logger.error(`Contribution already confirmd: ${id}`) throw new LogError('Contribution already confirmed', id)
throw new Error('Contribution already confirmd.')
} }
if (contribution.contributionStatus === 'DENIED') { if (contribution.contributionStatus === 'DENIED') {
logger.error(`Contribution already denied: ${id}`) throw new LogError('Contribution already denied', id)
throw new Error('Contribution already denied.')
} }
const moderatorUser = getUser(context) const moderatorUser = getUser(context)
if (moderatorUser.id === contribution.userId) { if (moderatorUser.id === contribution.userId) {
logger.error('Moderator can not confirm own contribution') throw new LogError('Moderator can not confirm own contribution')
throw new Error('Moderator can not confirm own contribution')
} }
const user = await DbUser.findOneOrFail( const user = await DbUser.findOneOrFail(
{ id: contribution.userId }, { id: contribution.userId },
{ withDeleted: true, relations: ['emailContact'] }, { withDeleted: true, relations: ['emailContact'] },
) )
if (user.deletedAt) { if (user.deletedAt) {
logger.error('This user was deleted. Cannot confirm a contribution.') throw new LogError('Can not confirm contribution since the user was deleted')
throw new Error('This user was deleted. Cannot confirm a contribution.')
} }
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false) const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
validateContribution( validateContribution(
@ -661,18 +598,12 @@ export class ContributionResolver {
}) })
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error('Creation was not successful', e) throw new LogError('Creation was not successful', e)
throw new Error('Creation was not successful.')
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
const event = new Event() await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
const eventContributionConfirm = new EventContributionConfirm()
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
} finally { } finally {
releaseLock() releaseLock()
} }
@ -737,17 +668,16 @@ export class ContributionResolver {
deniedBy: IsNull(), deniedBy: IsNull(),
}) })
if (!contributionToUpdate) { if (!contributionToUpdate) {
logger.error(`Contribution not found for given id: ${id}`) throw new LogError('Contribution not found', id)
throw new Error(`Contribution not found for given id.`)
} }
if ( if (
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS && contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
) { ) {
logger.error( throw new LogError(
`Contribution state (${contributionToUpdate.contributionStatus}) is not allowed.`, 'Status of the contribution is not allowed',
contributionToUpdate.contributionStatus,
) )
throw new Error(`State of the contribution is not allowed.`)
} }
const moderator = getUser(context) const moderator = getUser(context)
const user = await DbUser.findOne( const user = await DbUser.findOne(
@ -755,10 +685,7 @@ export class ContributionResolver {
{ relations: ['emailContact'] }, { relations: ['emailContact'] },
) )
if (!user) { if (!user) {
logger.error( throw new LogError('Could not find User of the Contribution', contributionToUpdate.userId)
`Could not find User for the Contribution (userId: ${contributionToUpdate.userId}).`,
)
throw new Error('Could not find User for the Contribution.')
} }
contributionToUpdate.contributionStatus = ContributionStatus.DENIED contributionToUpdate.contributionStatus = ContributionStatus.DENIED
@ -766,12 +693,12 @@ export class ContributionResolver {
contributionToUpdate.deniedAt = new Date() contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save() const res = await contributionToUpdate.save()
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_DENY(
const eventAdminContributionDeny = new EventAdminContributionDeny() contributionToUpdate.userId,
eventAdminContributionDeny.userId = contributionToUpdate.userId moderator.id,
eventAdminContributionDeny.amount = contributionToUpdate.amount contributionToUpdate.id,
eventAdminContributionDeny.contributionId = contributionToUpdate.id contributionToUpdate.amount,
await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny)) )
sendContributionDeniedEmail({ sendContributionDeniedEmail({
firstName: user.firstName, firstName: user.firstName,

View File

@ -16,6 +16,7 @@ import {
redeemTransactionLink, redeemTransactionLink,
createContribution, createContribution,
updateContribution, updateContribution,
createTransactionLink,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { listTransactionLinksAdmin } from '@/seeds/graphql/queries' import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
@ -24,6 +25,7 @@ import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { logger } from '@test/testSetup'
// mock semaphore to allow use fake timers // mock semaphore to allow use fake timers
jest.mock('@/util/TRANSACTIONS_LOCK') jest.mock('@/util/TRANSACTIONS_LOCK')
@ -50,7 +52,75 @@ afterAll(async () => {
}) })
describe('TransactionLinkResolver', () => { describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
it('throws error when amount is zero', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 0,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
it('throws error when amount is negative', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: -10,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 1001,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
describe('redeemTransactionLink', () => { describe('redeemTransactionLink', () => {
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('contributionLink', () => { describe('contributionLink', () => {
describe('input not valid', () => { describe('input not valid', () => {
beforeAll(async () => { beforeAll(async () => {
@ -61,6 +131,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('throws error when link does not exists', async () => { it('throws error when link does not exists', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -69,16 +140,26 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456',
),
],
}) })
}) })
it('throws error when link is not valid yet', async () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('No contribution link found to given code'),
)
})
const now = new Date() const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('throws error when link is not valid yet', async () => {
jest.clearAllMocks()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
} = await mutate({ } = await mutate({
@ -88,7 +169,7 @@ describe('TransactionLinkResolver', () => {
name: 'Daily Contribution Link', name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community', memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY', cycle: 'DAILY',
validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(), validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(), validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
@ -102,16 +183,21 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link not valid yet',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is not valid yet'),
)
})
it('throws error when contributionLink cycle is invalid', async () => { it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date() const now = new Date()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
@ -136,17 +222,22 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link has unknown cycle',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => { it('throws error when link is no longer valid', async () => {
const now = new Date() jest.clearAllMocks()
const { const {
data: { createContributionLink: contributionLink }, data: { createContributionLink: contributionLink },
} = await mutate({ } = await mutate({
@ -157,7 +248,7 @@ describe('TransactionLinkResolver', () => {
memo: 'Thank you for contribute daily to the community', memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY', cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(), validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(), validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200), maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1, maxPerCycle: 1,
}, },
@ -170,14 +261,18 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link is no longer valid',
),
],
}) })
await resetEntity(DbContributionLink) await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
}) })
// TODO: have this test separated into a transactionLink and a contributionLink part // TODO: have this test separated into a transactionLink and a contributionLink part
@ -250,6 +345,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link', async () => { it('does not allow the user to redeem the contribution link', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -258,13 +354,18 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
)
})
}) })
describe('user has no pending contributions that would not allow to redeem the link', () => { describe('user has no pending contributions that would not allow to redeem the link', () => {
@ -301,6 +402,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -309,14 +411,17 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError(
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
),
],
}) })
}) })
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
})
describe('after one day', () => { describe('after one day', () => {
beforeAll(async () => { beforeAll(async () => {
jest.useFakeTimers() jest.useFakeTimers()
@ -349,6 +454,7 @@ describe('TransactionLinkResolver', () => {
}) })
it('does not allow the user to redeem the contribution link a second time on the same day', async () => { it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -357,11 +463,15 @@ describe('TransactionLinkResolver', () => {
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [ errors: [new GraphQLError('Creation from contribution link was not successful')],
new GraphQLError( })
'Creation from contribution link was not successful. Error: Contribution link already redeemed today', })
),
], it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
}) })
}) })
}) })
@ -369,7 +479,7 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
describe('transaction links list', () => { describe('listTransactionLinksAdmin', () => {
const variables = { const variables = {
userId: 1, // dummy, may be replaced userId: 1, // dummy, may be replaced
filters: null, filters: null,
@ -377,8 +487,7 @@ describe('TransactionLinkResolver', () => {
pageSize: 5, pageSize: 5,
} }
// TODO: there is a test not cleaning up after itself! Fix it! afterAll(async () => {
beforeAll(async () => {
await cleanDB() await cleanDB()
resetToken() resetToken()
}) })
@ -436,9 +545,7 @@ describe('TransactionLinkResolver', () => {
variables.userId = user.id variables.userId = user.id
variables.pageSize = 25 variables.pageSize = 25
// bibi needs GDDs // bibi needs GDDs
const bibisCreation = creations.find( const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
(creation) => creation.email === 'bibi@bloxberg.de',
)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await creationFactory(testEnv, bibisCreation!) await creationFactory(testEnv, bibisCreation!)
// bibis transaktion links // bibis transaktion links
@ -638,7 +745,6 @@ describe('TransactionLinkResolver', () => {
}) })
}) })
}) })
})
describe('transactionLinkCode', () => { describe('transactionLinkCode', () => {
const date = new Date() const date = new Date()

View File

@ -32,6 +32,7 @@ import { getUserCreation, validateContribution } from './util/creations'
import { executeTransaction } from './TransactionResolver' import { executeTransaction } from './TransactionResolver'
import QueryLinkResult from '@union/QueryLinkResult' import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
@ -65,12 +66,16 @@ export class TransactionLinkResolver {
const createdDate = new Date() const createdDate = new Date()
const validUntil = transactionLinkExpireDate(createdDate) const validUntil = transactionLinkExpireDate(createdDate)
if (amount.lessThanOrEqualTo(0)) {
throw new LogError('Amount must be a positive number', amount)
}
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
// validate amount // validate amount
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
if (!sendBalance) { if (!sendBalance) {
throw new Error("user hasn't enough GDD or amount is < 0") throw new LogError('User has not enough GDD', user.id)
} }
const transactionLink = DbTransactionLink.create() const transactionLink = DbTransactionLink.create()
@ -186,24 +191,15 @@ export class TransactionLinkResolver {
.where('contributionLink.code = :code', { code: code.replace('CL-', '') }) .where('contributionLink.code = :code', { code: code.replace('CL-', '') })
.getOne() .getOne()
if (!contributionLink) { if (!contributionLink) {
logger.error('no contribution link found to given code:', code) throw new LogError('No contribution link found to given code', code)
throw new Error(`No contribution link found to given code: ${code}`)
} }
logger.info('...contribution link found with id', contributionLink.id) logger.info('...contribution link found with id', contributionLink.id)
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) { if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
logger.error( throw new LogError('Contribution link is not valid yet', contributionLink.validFrom)
'contribution link is not valid yet. Valid from: ',
contributionLink.validFrom,
)
throw new Error('Contribution link not valid yet')
} }
if (contributionLink.validTo) { if (contributionLink.validTo) {
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) { if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
logger.error( throw new LogError('Contribution link is no longer valid', contributionLink.validTo)
'contribution link is no longer valid. Valid to: ',
contributionLink.validTo,
)
throw new Error('Contribution link is no longer valid')
} }
} }
let alreadyRedeemed: DbContribution | undefined let alreadyRedeemed: DbContribution | undefined
@ -219,11 +215,7 @@ export class TransactionLinkResolver {
}) })
.getOne() .getOne()
if (alreadyRedeemed) { if (alreadyRedeemed) {
logger.error( throw new LogError('Contribution link already redeemed', user.id)
'contribution link with rule ONCE already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed')
} }
break break
} }
@ -248,17 +240,12 @@ export class TransactionLinkResolver {
) )
.getOne() .getOne()
if (alreadyRedeemed) { if (alreadyRedeemed) {
logger.error( throw new LogError('Contribution link already redeemed today', user.id)
'contribution link with rule DAILY already redeemed by user with id',
user.id,
)
throw new Error('Contribution link already redeemed today')
} }
break break
} }
default: { default: {
logger.error('contribution link has unknown cycle', contributionLink.cycle) throw new LogError('Contribution link has unknown cycle', contributionLink.cycle)
throw new Error('Contribution link has unknown cycle')
} }
} }
@ -308,8 +295,7 @@ export class TransactionLinkResolver {
logger.info('creation from contribution link commited successfuly.') logger.info('creation from contribution link commited successfuly.')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Creation from contribution link was not successful: ${e}`) throw new LogError('Creation from contribution link was not successful', e)
throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import Decimal from 'decimal.js-light'
import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocolType } from '@/event/EventProtocolType'
import { userFactory } from '@/seeds/factory/user' import { userFactory } from '@/seeds/factory/user'
import { import {
@ -118,10 +119,8 @@ describe('send coins', () => {
it('logs the error thrown', async () => { it('logs the error thrown', async () => {
// find peter to check the log // find peter to check the log
const user = await findUserByEmail(peterData.email) const user = await findUserByEmail('stephen@hawking.uk')
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
`The recipient account was deleted: recipientUser=${user}`,
)
}) })
}) })
@ -151,10 +150,8 @@ describe('send coins', () => {
it('logs the error thrown', async () => { it('logs the error thrown', async () => {
// find peter to check the log // find peter to check the log
const user = await findUserByEmail(peterData.email) const user = await findUserByEmail('garrick@ollivander.com')
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('The recipient account is not activated', user)
`The recipient account is not activated: recipientUser=${user}`,
)
}) })
}) })
}) })
@ -181,37 +178,13 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('Sender and Recipient are the same.')], errors: [new GraphQLError('Sender and Recipient are the same')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Sender and Recipient are the same.') expect(logger.error).toBeCalledWith('Sender and Recipient are the same', expect.any(Number))
})
})
describe('memo text is too long', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=256 > 255')
}) })
}) })
@ -229,13 +202,37 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('memo text is too short (5 characters minimum)')], errors: [new GraphQLError('Memo text is too short')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5') expect(logger.error).toBeCalledWith('Memo text is too short', 4)
})
})
describe('memo text is too long', () => {
it('throws an error', async () => {
jest.clearAllMocks()
expect(
await mutate({
mutation: sendCoins,
variables: {
email: 'peter@lustig.de',
amount: 100,
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
},
}),
).toEqual(
expect.objectContaining({
errors: [new GraphQLError('Memo text is too long')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Memo text is too long', 256)
}) })
}) })
@ -253,15 +250,13 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], errors: [new GraphQLError('User has not enough GDD or amount is < 0')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith('User has not enough GDD or amount is < 0', null)
`user hasn't enough GDD or amount is < 0 : balance=null`,
)
}) })
}) })
}) })
@ -293,6 +288,7 @@ describe('send coins', () => {
describe('trying to send negative amount', () => { describe('trying to send negative amount', () => {
it('throws an error', async () => { it('throws an error', async () => {
jest.clearAllMocks()
expect( expect(
await mutate({ await mutate({
mutation: sendCoins, mutation: sendCoins,
@ -304,13 +300,13 @@ describe('send coins', () => {
}), }),
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError(`Amount to send must be positive`)], errors: [new GraphQLError('Amount to send must be positive')],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(`Amount to send must be positive`) expect(logger.error).toBeCalledWith('Amount to send must be positive', new Decimal(-50))
}) })
}) })
@ -334,7 +330,7 @@ describe('send coins', () => {
) )
}) })
it('stores the send transaction event in the database', async () => { it('stores the TRANSACTION_SEND event in the database', async () => {
// Find the exact transaction (sent one is the one with user[1] as user) // Find the exact transaction (sent one is the one with user[1] as user)
const transaction = await Transaction.find({ const transaction = await Transaction.find({
userId: user[1].id, userId: user[1].id,
@ -351,7 +347,7 @@ describe('send coins', () => {
) )
}) })
it('stores the receive event in the database', async () => { it('stores the TRANSACTION_RECEIVE event in the database', async () => {
// Find the exact transaction (received one is the one with user[0] as user) // Find the exact transaction (received one is the one with user[0] as user)
const transaction = await Transaction.find({ const transaction = await Transaction.find({
userId: user[0].id, userId: user[0].id,

View File

@ -29,14 +29,14 @@ import {
sendTransactionLinkRedeemedEmail, sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail, sendTransactionReceivedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event'
import { writeEvent } from '@/event/EventProtocolEmitter'
import { BalanceResolver } from './BalanceResolver' import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { findUserByEmail } from './UserResolver' import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
@ -55,18 +55,15 @@ export const executeTransaction = async (
) )
if (sender.id === recipient.id) { if (sender.id === recipient.id) {
logger.error(`Sender and Recipient are the same.`) throw new LogError('Sender and Recipient are the same', sender.id)
throw new Error('Sender and Recipient are the same.')
}
if (memo.length > MEMO_MAX_CHARS) {
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
} }
if (memo.length < MEMO_MIN_CHARS) { if (memo.length < MEMO_MIN_CHARS) {
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`) throw new LogError('Memo text is too short', memo.length)
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`) }
if (memo.length > MEMO_MAX_CHARS) {
throw new LogError('Memo text is too long', memo.length)
} }
// validate amount // validate amount
@ -79,8 +76,7 @@ export const executeTransaction = async (
) )
logger.debug(`calculated Balance=${sendBalance}`) logger.debug(`calculated Balance=${sendBalance}`)
if (!sendBalance) { if (!sendBalance) {
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) throw new LogError('User has not enough GDD or amount is < 0', sendBalance)
throw new Error("user hasn't enough GDD or amount is < 0")
} }
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
@ -141,23 +137,22 @@ export const executeTransaction = async (
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`) logger.info(`commit Transaction successful...`)
const eventTransactionSend = new EventTransactionSend() await EVENT_TRANSACTION_SEND(
eventTransactionSend.userId = transactionSend.userId transactionSend.userId,
eventTransactionSend.xUserId = transactionSend.linkedUserId transactionSend.linkedUserId,
eventTransactionSend.transactionId = transactionSend.id transactionSend.id,
eventTransactionSend.amount = transactionSend.amount.mul(-1) transactionSend.amount.mul(-1),
await writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) )
const eventTransactionReceive = new EventTransactionReceive() await EVENT_TRANSACTION_RECEIVE(
eventTransactionReceive.userId = transactionReceive.userId transactionReceive.userId,
eventTransactionReceive.xUserId = transactionReceive.linkedUserId transactionReceive.linkedUserId,
eventTransactionReceive.transactionId = transactionReceive.id transactionReceive.id,
eventTransactionReceive.amount = transactionReceive.amount transactionReceive.amount,
await writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive)) )
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Transaction was not successful: ${e}`) throw new LogError('Transaction was not successful', e)
throw new Error(`Transaction was not successful: ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -316,8 +311,7 @@ export class TransactionResolver {
): Promise<boolean> { ): Promise<boolean> {
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
if (amount.lte(0)) { if (amount.lte(0)) {
logger.error(`Amount to send must be positive`) throw new LogError('Amount to send must be positive', amount)
throw new Error('Amount to send must be positive')
} }
// TODO this is subject to replay attacks // TODO this is subject to replay attacks
@ -326,13 +320,11 @@ export class TransactionResolver {
// validate recipient user // validate recipient user
const recipientUser = await findUserByEmail(email) const recipientUser = await findUserByEmail(email)
if (recipientUser.deletedAt) { if (recipientUser.deletedAt) {
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) throw new LogError('The recipient account was deleted', recipientUser)
throw new Error('The recipient account was deleted')
} }
const emailContact = recipientUser.emailContact const emailContact = recipientUser.emailContact
if (!emailContact.emailChecked) { if (!emailContact.emailChecked) {
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`) throw new LogError('The recipient account is not activated', recipientUser)
throw new Error('The recipient account is not activated')
} }
await executeTransaction(amount, memo, senderUser, recipientUser) await executeTransaction(amount, memo, senderUser, recipientUser)

View File

@ -19,6 +19,7 @@ import {
setUserRole, setUserRole,
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
sendActivationEmail,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries' import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
@ -175,6 +176,19 @@ describe('UserResolver', () => {
}) })
}) })
}) })
it('stores the REGISTER event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REGISTER,
userId: userConatct.user.id,
}),
)
})
}) })
describe('account activation email', () => { describe('account activation email', () => {
@ -196,7 +210,7 @@ describe('UserResolver', () => {
}) })
}) })
it('stores the send confirmation event in the database', () => { it('stores the SEND_CONFIRMATION_EMAIL event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
@ -206,7 +220,7 @@ describe('UserResolver', () => {
}) })
}) })
describe('email already exists', () => { describe('user already exists', () => {
let mutation: User let mutation: User
beforeAll(async () => { beforeAll(async () => {
mutation = await mutate({ mutation: createUser, variables }) mutation = await mutate({ mutation: createUser, variables })
@ -236,6 +250,19 @@ describe('UserResolver', () => {
}), }),
) )
}) })
it('stores the SEND_ACCOUNT_MULTIREGISTRATION_EMAIL event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
userId: userConatct.user.id,
}),
)
})
}) })
describe('unknown language', () => { describe('unknown language', () => {
@ -328,7 +355,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the account activated event in the database', () => { it('stores the ACTIVATE_ACCOUNT event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT, type: EventProtocolType.ACTIVATE_ACCOUNT,
@ -337,7 +364,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the redeem register event in the database', () => { it('stores the REDEEM_REGISTER event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventProtocolType.REDEEM_REGISTER,
@ -421,7 +448,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the redeem register event in the database', async () => { it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventProtocolType.REDEEM_REGISTER,
@ -647,6 +674,19 @@ describe('UserResolver', () => {
it('sets the token in the header', () => { it('sets the token in the header', () => {
expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) }) expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) })
}) })
it('stores the LOGIN event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
userId: userConatct.user.id,
}),
)
})
}) })
describe('user is in database and wrong password', () => { describe('user is in database and wrong password', () => {
@ -887,7 +927,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the login event in the database', () => { it('stores the LOGIN event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.LOGIN, type: EventProtocolType.LOGIN,
@ -1668,6 +1708,157 @@ describe('UserResolver', () => {
}) })
}) })
///
describe('sendActivationEmail', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with admin rights', () => {
beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('user does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'INVALID' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No user with this credentials', 'invalid')
})
})
describe('user is deleted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await userFactory(testEnv, stephenHawking)
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'stephen@hawking.uk' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User with given email contact is deleted')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'User with given email contact is deleted',
'stephen@hawking.uk',
)
})
})
describe('sendActivationEmail with success', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
})
it('returns true', async () => {
const result = await mutate({
mutation: sendActivationEmail,
variables: { email: 'bibi@bloxberg.de' },
})
expect(result).toEqual(
expect.objectContaining({
data: {
sendActivationEmail: true,
},
}),
)
})
it('sends an account activation email', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
userConatct.emailVerificationCode.toString(),
).replace(/{code}/g, '')
expect(sendAccountActivationEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
activationLink,
timeDurationObject: expect.objectContaining({
hours: expect.any(Number),
minutes: expect.any(Number),
}),
})
})
it('stores the ADMIN_SEND_CONFIRMATION_EMAIL event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL,
userId: userConatct.user.id,
}),
)
})
})
})
})
})
describe('unDelete user', () => { describe('unDelete user', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {

View File

@ -48,15 +48,14 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
import { klicktippSignIn } from '@/apis/KlicktippController' import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys' import { hasElopageBuys } from '@/util/hasElopageBuys'
import { writeEvent } from '@/event/EventProtocolEmitter'
import { import {
Event, Event,
EventLogin, EVENT_LOGIN,
EventRedeemRegister, EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
EventRegister, EVENT_SEND_CONFIRMATION_EMAIL,
EventSendAccountMultiRegistrationEmail, EVENT_REGISTER,
EventSendConfirmationEmail, EVENT_ACTIVATE_ACCOUNT,
EventActivateAccount, EVENT_ADMIN_SEND_CONFIRMATION_EMAIL,
} from '@/event/Event' } from '@/event/Event'
import { getUserCreations } from './util/creations' import { getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils' import { isValidPassword } from '@/password/EncryptorUtils'
@ -64,6 +63,7 @@ import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { EventProtocolType } from '@/event/EventProtocolType'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native') const sodium = require('sodium-native')
@ -177,9 +177,8 @@ export class UserResolver {
key: 'token', key: 'token',
value: encode(dbUser.gradidoID), value: encode(dbUser.gradidoID),
}) })
const ev = new EventLogin()
ev.userId = user.id await EVENT_LOGIN(user.id)
writeEvent(new Event().setEventLogin(ev))
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user return user
} }
@ -211,7 +210,6 @@ export class UserResolver {
) )
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0; // default int publisher_id = 0;
const event = new Event()
// Validate Language (no throw) // Validate Language (no throw)
if (!language || !isLanguage(language)) { if (!language || !isLanguage(language)) {
@ -249,9 +247,9 @@ export class UserResolver {
email, email,
language: foundUser.language, // use language of the emails owner for sending language: foundUser.language, // use language of the emails owner for sending
}) })
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
eventSendAccountMultiRegistrationEmail.userId = foundUser.id await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser.id)
writeEvent(event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail))
logger.info( logger.info(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`, `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
) )
@ -268,10 +266,7 @@ export class UserResolver {
const gradidoID = await newGradidoID() const gradidoID = await newGradidoID()
const eventRegister = new EventRegister() const eventRegisterRedeem = Event(EventProtocolType.REDEEM_REGISTER, 0)
const eventRedeemRegister = new EventRedeemRegister()
const eventSendConfirmEmail = new EventSendConfirmationEmail()
let dbUser = new DbUser() let dbUser = new DbUser()
dbUser.gradidoID = gradidoID dbUser.gradidoID = gradidoID
dbUser.firstName = firstName dbUser.firstName = firstName
@ -288,14 +283,14 @@ export class UserResolver {
logger.info('redeemCode found contributionLink=' + contributionLink) logger.info('redeemCode found contributionLink=' + contributionLink)
if (contributionLink) { if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id dbUser.contributionLinkId = contributionLink.id
eventRedeemRegister.contributionId = contributionLink.id eventRegisterRedeem.contributionId = contributionLink.id
} }
} else { } else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode }) const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink) logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) { if (transactionLink) {
dbUser.referrerId = transactionLink.userId dbUser.referrerId = transactionLink.userId
eventRedeemRegister.transactionId = transactionLink.id eventRegisterRedeem.transactionId = transactionLink.id
} }
} }
} }
@ -333,8 +328,8 @@ export class UserResolver {
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
eventSendConfirmEmail.userId = dbUser.id
writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail)) await EVENT_SEND_CONFIRMATION_EMAIL(dbUser.id)
if (!emailSent) { if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`) logger.debug(`Account confirmation link: ${activationLink}`)
@ -351,11 +346,10 @@ export class UserResolver {
logger.info('createUser() successful...') logger.info('createUser() successful...')
if (redeemCode) { if (redeemCode) {
eventRedeemRegister.userId = dbUser.id eventRegisterRedeem.userId = dbUser.id
await writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) await eventRegisterRedeem.save()
} else { } else {
eventRegister.userId = dbUser.id await EVENT_REGISTER(dbUser.id)
await writeEvent(event.setEventRegister(eventRegister))
} }
return new User(dbUser) return new User(dbUser)
@ -458,8 +452,6 @@ export class UserResolver {
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') await queryRunner.startTransaction('REPEATABLE READ')
const event = new Event()
try { try {
// Save user // Save user
await queryRunner.manager.save(user).catch((error) => { await queryRunner.manager.save(user).catch((error) => {
@ -473,9 +465,7 @@ export class UserResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('User and UserContact data written successfully...') logger.info('User and UserContact data written successfully...')
const eventActivateAccount = new EventActivateAccount() await EVENT_ACTIVATE_ACCOUNT(user.id)
eventActivateAccount.userId = user.id
writeEvent(event.setEventActivateAccount(eventActivateAccount))
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e) throw new LogError('Error on writing User and User Contact data', e)
@ -791,19 +781,12 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
// const user = await dbUser.findOne({ id: emailContact.userId }) // const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email) const user = await findUserByEmail(email)
if (!user) { if (user.deletedAt || user.emailContact.deletedAt) {
throw new LogError('Could not find user to given email contact', email)
}
if (user.deletedAt) {
throw new LogError('User with given email contact is deleted', email) throw new LogError('User with given email contact is deleted', email)
} }
const emailContact = user.emailContact
if (emailContact.deletedAt) {
throw new LogError('The given email contact for this user is deleted', email)
}
emailContact.emailResendCount++ user.emailContact.emailResendCount++
await emailContact.save() await user.emailContact.save()
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({ const emailSent = await sendAccountActivationEmail({
@ -811,7 +794,7 @@ export class UserResolver {
lastName: user.lastName, lastName: user.lastName,
email, email,
language: user.language, language: user.language,
activationLink: activationLink(emailContact.emailVerificationCode), activationLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
@ -819,10 +802,7 @@ export class UserResolver {
if (!emailSent) { if (!emailSent) {
logger.info(`Account confirmation link: ${activationLink}`) logger.info(`Account confirmation link: ${activationLink}`)
} else { } else {
const event = new Event() await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id)
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
eventSendConfirmationEmail.userId = user.id
await writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmationEmail))
} }
return true return true

View File

@ -4,6 +4,7 @@ import createServer from './server/createServer'
// config // config
import CONFIG from './config' import CONFIG from './config'
import { startValidateCommunities } from './federation/validateCommunities'
async function main() { async function main() {
const { app } = await createServer() const { app } = await createServer()
@ -16,6 +17,7 @@ async function main() {
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`) console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
} }
}) })
startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
} }
main().catch((e) => { main().catch((e) => {

View File

@ -1,10 +1,5 @@
{ {
"emails": { "emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"accountActivation": { "accountActivation": {
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:", "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
@ -19,6 +14,11 @@
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
}, },
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"contributionConfirmed": { "contributionConfirmed": {
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"

View File

@ -1,10 +1,5 @@
{ {
"emails": { "emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"accountActivation": { "accountActivation": {
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:", "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
"emailRegistered": "Your email address has just been registered with Gradido.", "emailRegistered": "Your email address has just been registered with Gradido.",
@ -19,6 +14,11 @@
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
"subject": "Gradido: Try To Register Again With Your Email" "subject": "Gradido: Try To Register Again With Your Email"
}, },
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"contributionConfirmed": { "contributionConfirmed": {
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
"subject": "Gradido: Your contribution to the common good was confirmed" "subject": "Gradido: Your contribution to the common good was confirmed"

View File

@ -68,6 +68,12 @@ export const createUser = gql`
} }
` `
export const sendActivationEmail = gql`
mutation ($email: String!) {
sendActivationEmail(email: $email)
}
`
export const sendCoins = gql` export const sendCoins = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!) { mutation ($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo) sendCoins(email: $email, amount: $amount, memo: $memo)

View File

@ -404,6 +404,11 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@graphql-typed-document-node/core@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
"@hapi/boom@^10.0.0": "@hapi/boom@^10.0.0":
version "10.0.0" version "10.0.0"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.0.tgz#3624831d0a26b3378423b246f50eacea16e04a08" resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.0.tgz#3624831d0a26b3378423b246f50eacea16e04a08"
@ -430,42 +435,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@hyperswarm/dht@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@hyperswarm/dht/-/dht-6.2.0.tgz#b2cb1218752b52fabb66f304e73448a108d1effd"
integrity sha512-AeyfRdAkfCz/J3vTC4rdpzEpT7xQ+tls87Zpzw9Py3VGUZD8hMT7pr43OOdkCBNvcln6K/5/Lxhnq5lBkzH3yw==
dependencies:
"@hyperswarm/secret-stream" "^6.0.0"
b4a "^1.3.1"
bogon "^1.0.0"
compact-encoding "^2.4.1"
compact-encoding-net "^1.0.1"
debugging-stream "^2.0.0"
dht-rpc "^6.0.0"
events "^3.3.0"
hypercore-crypto "^3.3.0"
noise-curve-ed "^1.0.2"
noise-handshake "^2.1.0"
record-cache "^1.1.1"
safety-catch "^1.0.1"
sodium-universal "^3.0.4"
udx-native "^1.1.0"
xache "^1.1.0"
"@hyperswarm/secret-stream@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@hyperswarm/secret-stream/-/secret-stream-6.0.0.tgz#67db820308cc9fed899cb8f5e9f47ae819d5a4e3"
integrity sha512-0xuyJIJDe8JYk4uWUx25qJvWqybdjKU2ZIfP1GTqd7dQxwdR0bpYrQKdLkrn5txWSK4a28ySC2AjH0G3I0gXTA==
dependencies:
b4a "^1.1.0"
hypercore-crypto "^3.3.0"
noise-curve-ed "^1.0.2"
noise-handshake "^2.1.0"
sodium-secretstream "^1.0.0"
sodium-universal "^3.0.4"
streamx "^2.10.2"
timeout-refresh "^2.0.0"
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1655,11 +1624,6 @@ axios@^0.21.1:
dependencies: dependencies:
follow-redirects "^1.14.0" follow-redirects "^1.14.0"
b4a@^1.0.1, b4a@^1.1.0, b4a@^1.1.1, b4a@^1.3.0, b4a@^1.3.1, b4a@^1.5.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.5.3.tgz#56293b5607aeda3fd81c481e516e9f103fc88341"
integrity sha512-1aCQIzQJK7G0z1Una75tWMlwVAR8o+QHoAlnWc5XAxRVBESY9WsitfBgM5nPyDBP5HrhPU1Np4Pq2Y7CJQ+tVw==
babel-jest@^27.2.5: babel-jest@^27.2.5:
version "27.2.5" version "27.2.5"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa"
@ -1743,22 +1707,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
blake2b-wasm@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz#9115649111edbbd87eb24ce7c04b427e4e2be5be"
integrity sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
blake2b@^2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.4.tgz#817d278526ddb4cd673bfb1af16d1ad61e393ba3"
integrity sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==
dependencies:
blake2b-wasm "^2.4.0"
nanoassert "^2.0.0"
bluebird@^3.7.2: bluebird@^3.7.2:
version "3.7.2" version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@ -1780,11 +1728,6 @@ body-parser@1.19.0, body-parser@^1.18.3:
raw-body "2.4.0" raw-body "2.4.0"
type-is "~1.6.17" type-is "~1.6.17"
bogon@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/bogon/-/bogon-1.0.0.tgz#66b8cdd269f790e3aa988e157bb34d4ba75ee586"
integrity sha512-mXxtlBtnW8koqFWPUBtKJm97vBSKZRpOvxvMRVun33qQXwMNfQzq9eTcQzKzqEoNUhNqF9t8rDc/wakKCcHMTg==
boolbase@^1.0.0: boolbase@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -1917,13 +1860,6 @@ caniuse-lite@^1.0.30001264:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow== integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
chacha20-universal@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/chacha20-universal/-/chacha20-universal-1.0.4.tgz#e8a33a386500b1ce5361b811ec5e81f1797883f5"
integrity sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q==
dependencies:
nanoassert "^2.0.0"
chalk@^2.0.0: chalk@^2.0.0:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -2093,20 +2029,6 @@ commander@^6.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
compact-encoding-net@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/compact-encoding-net/-/compact-encoding-net-1.0.1.tgz#4da743d52721f5d0cc73a6d00556a96bc9b9fa1b"
integrity sha512-N9k1Qwg9b1ENk+TZsZhthzkuMtn3rn4ZinN75gf3/LplE+uaTCKjyaau5sK0m2NEUa/MmR77VxiGfD/Qz1ar0g==
dependencies:
compact-encoding "^2.4.1"
compact-encoding@^2.1.0, compact-encoding@^2.4.1, compact-encoding@^2.5.1:
version "2.7.0"
resolved "https://registry.yarnpkg.com/compact-encoding/-/compact-encoding-2.7.0.tgz#e6a0df408c25cbcdf7d619c97527074478cafd06"
integrity sha512-2I0A+pYKXYwxewbLxj26tU4pJyKlFNjadzjZ+36xJ5HwTrnhD9KcMQk3McEQRl1at6jrwA8E7UjmBdsGhEAPMw==
dependencies:
b4a "^1.3.0"
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2193,6 +2115,13 @@ cross-env@^7.0.3:
dependencies: dependencies:
cross-spawn "^7.0.1" cross-spawn "^7.0.1"
cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
cross-spawn@^6.0.0: cross-spawn@^6.0.0:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -2305,13 +2234,6 @@ debug@^4.3.3, debug@^4.3.4:
dependencies: dependencies:
ms "2.1.2" ms "2.1.2"
debugging-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/debugging-stream/-/debugging-stream-2.0.0.tgz#515cad5a35299cf4b4bc0afcbd69d52c809c84ce"
integrity sha512-xwfl6wB/3xc553uwtGnSa94jFxnGOc02C0WU2Nmzwr80gzeqn1FX4VcbvoKIhe8L/lPq4BTQttAbrTN94uN8rA==
dependencies:
streamx "^2.12.4"
decimal.js-light@^2.5.1: decimal.js-light@^2.5.1:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
@ -2391,23 +2313,6 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
dht-rpc@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/dht-rpc/-/dht-rpc-6.1.1.tgz#a292a22aa19b05136978d33528cb571d6e32502f"
integrity sha512-wo0nMXwn/rhxVz62V0d+l/0HuikxLQh6lkwlUIdoaUzGl9DobFj4epSScD3/lTMwKts+Ih0DFNqP+j0tYwdajQ==
dependencies:
b4a "^1.3.1"
compact-encoding "^2.1.0"
compact-encoding-net "^1.0.1"
events "^3.3.0"
fast-fifo "^1.0.0"
kademlia-routing-table "^1.0.0"
nat-sampler "^1.0.1"
sodium-universal "^3.0.4"
streamx "^2.10.3"
time-ordered-set "^1.0.2"
udx-native "^1.1.0"
dicer@0.3.0: dicer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
@ -2899,11 +2804,6 @@ eventemitter3@^3.1.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
execa@^0.10.0: execa@^0.10.0:
version "0.10.0" version "0.10.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
@ -2985,6 +2885,11 @@ express@^4.17.1:
utils-merge "1.0.1" utils-merge "1.0.1"
vary "~1.1.2" vary "~1.1.2"
extract-files@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a"
integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==
faker@^5.5.3: faker@^5.5.3:
version "5.5.3" version "5.5.3"
resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e" resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e"
@ -3000,11 +2905,6 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-fifo@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.1.0.tgz#17d1a3646880b9891dfa0c54e69c5fef33cad779"
integrity sha512-Kl29QoNbNvn4nhDsLYjyIAaIqaJB6rBx5p3sL9VjaefJ+eMFBWVZiaoguaoZfzEKr5RhAti0UgM8703akGPJ6g==
fast-glob@^3.1.1: fast-glob@^3.1.1:
version "3.2.7" version "3.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
@ -3340,6 +3240,16 @@ graphql-query-complexity@^0.7.0:
dependencies: dependencies:
lodash.get "^4.4.2" lodash.get "^4.4.2"
graphql-request@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-5.0.0.tgz#7504a807d0e11be11a3c448e900f0cc316aa18ef"
integrity sha512-SpVEnIo2J5k2+Zf76cUkdvIRaq5FMZvGQYnA4lUWYbc99m+fHh4CZYRRO/Ff4tCLQ613fzCm3SiDT64ubW5Gyw==
dependencies:
"@graphql-typed-document-node/core" "^3.1.1"
cross-fetch "^3.1.5"
extract-files "^9.0.0"
form-data "^3.0.0"
graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0: graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d"
@ -3414,15 +3324,6 @@ he@1.2.0, he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hmac-blake2b@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hmac-blake2b/-/hmac-blake2b-2.0.0.tgz#09494e5d245d7afe45d157093080b159f7bacf15"
integrity sha512-JbGNtM1YRd8EQH/2vNTAP1oy5lJVPlBFYZfCJTu3k8sqOUm0rRIf/3+MCd5noVykETwTbun6jEOc+4Tu78ubHA==
dependencies:
nanoassert "^1.1.0"
sodium-native "^3.1.1"
sodium-universal "^3.0.0"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.9" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -3544,15 +3445,6 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
hypercore-crypto@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/hypercore-crypto/-/hypercore-crypto-3.3.0.tgz#03ab5b44608a563e131f629f671c6f90a83c52e6"
integrity sha512-zAWbDqG7kWwS6rCxxTUeB/OeFAz3PoOmouKaoMubtDJYJsLHqXtA3wE2mLsw+E2+iYyom5zrFyBTFVYxmgwW6g==
dependencies:
b4a "^1.1.0"
compact-encoding "^2.5.1"
sodium-universal "^3.0.0"
i18n-locales@^0.0.5: i18n-locales@^0.0.5:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.5.tgz#8f587e598ab982511d7c7db910cb45b8d93cd96a" resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.5.tgz#8f587e598ab982511d7c7db910cb45b8d93cd96a"
@ -4517,11 +4409,6 @@ jws@^3.2.2:
jwa "^1.4.1" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
kademlia-routing-table@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/kademlia-routing-table/-/kademlia-routing-table-1.0.1.tgz#6f18416f612e885a8d4df128f04c490a90d772f6"
integrity sha512-dKk19sC3/+kWhBIvOKCthxVV+JH0NrswSBq4sA4eOkkPMqQM1rRuOWte1WSKXeP8r9Nx4NuiH2gny3lMddJTpw==
keyv@^3.0.0: keyv@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@ -4932,26 +4819,6 @@ named-placeholders@^1.1.2:
dependencies: dependencies:
lru-cache "^4.1.3" lru-cache "^4.1.3"
nanoassert@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d"
integrity sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==
nanoassert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-2.0.0.tgz#a05f86de6c7a51618038a620f88878ed1e490c09"
integrity sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==
napi-macros@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
nat-sampler@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nat-sampler/-/nat-sampler-1.0.1.tgz#2b68338ea6d4c139450cd971fd00a4ac1b33d923"
integrity sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -4977,7 +4844,7 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-fetch@^2.6.0: node-fetch@2.6.7, node-fetch@^2.6.0:
version "2.6.7" version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -4991,7 +4858,7 @@ node-fetch@^2.6.1:
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
node-gyp-build@^4.3.0, node-gyp-build@^4.4.0: node-gyp-build@^4.3.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
@ -5042,25 +4909,6 @@ nodemon@^2.0.7:
undefsafe "^2.0.3" undefsafe "^2.0.3"
update-notifier "^5.1.0" update-notifier "^5.1.0"
noise-curve-ed@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/noise-curve-ed/-/noise-curve-ed-1.0.4.tgz#8ae83f5d2d2e31d0c9c069271ca6e462d31cd884"
integrity sha512-plUUSEOU66FZ9TaBKpk4+fgQeeS+OLlThS2o8a1TxVpMWV2v1izvEnjSpFV9gEPZl4/1yN+S5KqLubFjogqQOw==
dependencies:
b4a "^1.1.0"
nanoassert "^2.0.0"
sodium-universal "^3.0.4"
noise-handshake@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/noise-handshake/-/noise-handshake-2.2.0.tgz#24c98f502d49118770e1ec2af2894b8789f0ac7c"
integrity sha512-+0mFUc5YSnOPI+4K/7nr6XDGduITaUasPVurzrH03sk6yW+udKxP/qjEwEekRwIpnvcCKYnjiZ9HJenJv9ljZg==
dependencies:
b4a "^1.1.0"
hmac-blake2b "^2.0.0"
nanoassert "^2.0.0"
sodium-universal "^3.0.4"
nopt@~1.0.10: nopt@~1.0.10:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
@ -5666,11 +5514,6 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
queue-tick@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.0.tgz#011104793a3309ae86bfeddd54e251dc94a36725"
integrity sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==
railroad-diagrams@^1.0.0: railroad-diagrams@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
@ -5743,13 +5586,6 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
record-cache@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.2.0.tgz#e601bc4f164d58330cc00055e27aa4682291c882"
integrity sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==
dependencies:
b4a "^1.3.1"
reflect-metadata@^0.1.13: reflect-metadata@^0.1.13:
version "0.1.13" version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@ -5809,7 +5645,7 @@ resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0:
is-core-module "^2.2.0" is-core-module "^2.2.0"
path-parse "^1.0.6" path-parse "^1.0.6"
resolve@^1.15.1, resolve@^1.17.0: resolve@^1.15.1:
version "1.22.1" version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
@ -5886,11 +5722,6 @@ safe-identifier@^0.4.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
safety-catch@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/safety-catch/-/safety-catch-1.0.2.tgz#d64cbd57fd601da91c356b6ab8902f3e449a7a4b"
integrity sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==
saxes@^5.0.1: saxes@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@ -5981,38 +5812,6 @@ sha.js@^2.4.11:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha256-universal@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sha256-universal/-/sha256-universal-1.2.1.tgz#051d92decce280cd6137d42d496eac88da942c0e"
integrity sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw==
dependencies:
b4a "^1.0.1"
sha256-wasm "^2.2.1"
sha256-wasm@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/sha256-wasm/-/sha256-wasm-2.2.2.tgz#4940b6c9ba28f3f08b700efce587ef36d4d516d4"
integrity sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
sha512-universal@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sha512-universal/-/sha512-universal-1.2.1.tgz#829505a7586530515cc1a10b78815c99722c4df0"
integrity sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ==
dependencies:
b4a "^1.0.1"
sha512-wasm "^2.3.1"
sha512-wasm@^2.3.1:
version "2.3.4"
resolved "https://registry.yarnpkg.com/sha512-wasm/-/sha512-wasm-2.3.4.tgz#b86b37112ff6d1fc3740f2484a6855f17a6e1300"
integrity sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
shebang-command@^1.2.0: shebang-command@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -6056,13 +5855,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
siphash24@^1.0.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/siphash24/-/siphash24-1.3.1.tgz#7f87fd2c5db88d8d46335a68f780f281641c8b22"
integrity sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA==
dependencies:
nanoassert "^2.0.0"
sisteransi@^1.0.5: sisteransi@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -6087,50 +5879,13 @@ slick@^1.12.2:
resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7"
integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==
sodium-javascript@~0.8.0: sodium-native@^3.3.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/sodium-javascript/-/sodium-javascript-0.8.0.tgz#0a94d7bb58ab17be82255f3949259af59778fdbc"
integrity sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg==
dependencies:
blake2b "^2.1.1"
chacha20-universal "^1.0.4"
nanoassert "^2.0.0"
sha256-universal "^1.1.0"
sha512-universal "^1.1.0"
siphash24 "^1.0.1"
xsalsa20 "^1.0.0"
sodium-native@^3.1.1, sodium-native@^3.2.0, sodium-native@^3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361" resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361"
integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA== integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA==
dependencies: dependencies:
node-gyp-build "^4.3.0" node-gyp-build "^4.3.0"
sodium-secretstream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/sodium-secretstream/-/sodium-secretstream-1.0.2.tgz#ae6fec16555f1a1d9fd2460b41256736d5044e13"
integrity sha512-AsWztbBHhHid+w5g28ftXA0mTrS52Dup7FYI0GR7ri1TQTlVsw0z//FNlhIqWsgtBctO/DxQosacbElCpmdcZw==
dependencies:
b4a "^1.1.1"
sodium-universal "^3.0.4"
sodium-universal@^3.0.0, sodium-universal@^3.0.4:
version "3.1.0"
resolved "https://registry.yarnpkg.com/sodium-universal/-/sodium-universal-3.1.0.tgz#f2fa0384d16b7cb99b1c8551a39cc05391a3ed41"
integrity sha512-N2gxk68Kg2qZLSJ4h0NffEhp4BjgWHCHXVlDi1aG1hA3y+ZeWEmHqnpml8Hy47QzfL1xLy5nwr9LcsWAg2Ep0A==
dependencies:
blake2b "^2.1.1"
chacha20-universal "^1.0.4"
nanoassert "^2.0.0"
resolve "^1.17.0"
sha256-universal "^1.1.0"
sha512-universal "^1.1.0"
siphash24 "^1.0.1"
sodium-javascript "~0.8.0"
sodium-native "^3.2.0"
xsalsa20 "^1.0.0"
source-map-support@^0.5.6: source-map-support@^0.5.6:
version "0.5.20" version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
@ -6216,14 +5971,6 @@ streamsearch@0.1.2:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
streamx@^2.10.2, streamx@^2.10.3, streamx@^2.12.0, streamx@^2.12.4:
version "2.12.4"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.12.4.tgz#0369848b20b8f79c65320735372df17cafcd9aff"
integrity sha512-K3xdIp8YSkvbdI0PrCcP0JkniN8cPCyeKlcZgRFSl1o1xKINCYM93FryvTSOY57x73pz5/AjO5B8b9BYf21wWw==
dependencies:
fast-fifo "^1.0.0"
queue-tick "^1.0.0"
string-length@^4.0.1: string-length@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@ -6388,16 +6135,6 @@ throat@^6.0.1:
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
time-ordered-set@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/time-ordered-set/-/time-ordered-set-1.0.2.tgz#3bd931fc048234147f8c2b8b1ebbebb0a3ecb96f"
integrity sha512-vGO99JkxvgX+u+LtOKQEpYf31Kj3i/GNwVstfnh4dyINakMgeZCpew1e3Aj+06hEslhtHEd52g7m5IV+o1K8Mw==
timeout-refresh@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-2.0.1.tgz#f8ec7cf1f9d93b2635b7d4388cb820c5f6c16f98"
integrity sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==
titleize@2: titleize@2:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f" resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f"
@ -6622,16 +6359,6 @@ uc.micro@^1.0.1:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
udx-native@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/udx-native/-/udx-native-1.2.1.tgz#a229b8bfab8c9c9eea05c7e0d68e671ab70d562d"
integrity sha512-hLoJ3rE1PuqO/A1YENG8oYNuAGltdwXofzavYwXbg2yk/qQgGBDpUQd/qtdENxkawad5cEEdJEdwvchslDl7OA==
dependencies:
b4a "^1.5.0"
napi-macros "^2.0.0"
node-gyp-build "^4.4.0"
streamx "^2.12.0"
unbox-primitive@^1.0.1: unbox-primitive@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@ -6936,11 +6663,6 @@ write-file-atomic@^3.0.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
xache@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/xache/-/xache-1.1.0.tgz#afc20dec9ff8b2260eea03f5ad9422dc0200c6e9"
integrity sha512-RQGZDHLy/uCvnIrAvaorZH/e6Dfrtxj16iVlGjkj4KD2/G/dNXNqhk5IdSucv5nSSnDK00y8Y/2csyRdHveJ+Q==
xdg-basedir@^4.0.0: xdg-basedir@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
@ -6956,11 +6678,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xsalsa20@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/xsalsa20/-/xsalsa20-1.2.0.tgz#e5a05cb26f8cef723f94a559102ed50c1b44c25c"
integrity sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==
xss@^1.0.8: xss@^1.0.8:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2" resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2"

View File

@ -0,0 +1,51 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('communities')
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
publicKey: Buffer
@Column({ name: 'api_version', length: 10, nullable: false })
apiVersion: string
@Column({ name: 'end_point', length: 255, nullable: false })
endPoint: string
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
lastAnnouncedAt: Date
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
verifiedAt: Date
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
lastErrorAt: Date
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
}

View File

@ -1 +1 @@
export { Community } from './0058-add_communities_table/Community' export { Community } from './0060-update_communities_table/Community'

View File

@ -0,0 +1,32 @@
/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES
*
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
*/
/* 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 `communities` MODIFY COLUMN `last_announced_at` datetime(3) AFTER `end_point`;',
)
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `foreign` tinyint(4) NOT NULL DEFAULT 1 AFTER `id`;',
)
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `verified_at` datetime(3) AFTER `last_announced_at`;',
)
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `last_error_at` datetime(3) AFTER `verified_at`;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(
'ALTER TABLE `communities` MODIFY COLUMN `last_announced_at` datetime(3) NOT NULL AFTER `end_point`;',
)
await queryFn('ALTER TABLE `communities` DROP COLUMN `foreign`;')
await queryFn('ALTER TABLE `communities` DROP COLUMN `verified_at`;')
await queryFn('ALTER TABLE `communities` DROP COLUMN `last_error_at`;')
}

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.18.1", "version": "1.18.2",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -15,3 +15,5 @@ TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log
# on an hash created from this topic # on an hash created from this topic
FEDERATION_DHT_TOPIC=GRADIDO_HUB FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_COMMUNITY_URL=http://localhost
# FEDERATION_COMMUNITY_API_PORT=5000

View File

@ -12,3 +12,4 @@ TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
FEDERATION_COMMUNITY_API_PORT=$FEDERATION_COMMUNITY_API_PORT

View File

@ -3,7 +3,7 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0059-add_hide_amount_to_users', DB_VERSION: '0060-update_communities_table',
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
@ -31,7 +31,8 @@ const database = {
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || 'http://localhost',
FEDERATION_COMMUNITY_API_PORT: process.env.FEDERATION_COMMUNITY_API_PORT || '5000',
} }
// Check config version // Check config version

View File

@ -116,6 +116,7 @@ describe('federation', () => {
beforeEach(async () => { beforeEach(async () => {
DHT.mockClear() DHT.mockClear()
jest.clearAllMocks() jest.clearAllMocks()
await cleanDB()
await startDHT(TEST_TOPIC) await startDHT(TEST_TOPIC)
}) })
@ -234,18 +235,18 @@ describe('federation', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
jsonArray = [ jsonArray = [
{ api: 'v1_0', url: 'too much versions at the same time test' }, { api: '1_0', url: 'too much versions at the same time test' },
{ api: 'v1_0', url: 'url2' }, { api: '1_0', url: 'url2' },
{ api: 'v1_0', url: 'url3' }, { api: '1_0', url: 'url3' },
{ api: 'v1_0', url: 'url4' }, { api: '1_0', url: 'url4' },
{ api: 'v1_0', url: 'url5' }, { api: '1_0', url: 'url5' },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
}) })
it('logs the received data', () => { it('logs the received data', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
'data: [{"api":"v1_0","url":"too much versions at the same time test"},{"api":"v1_0","url":"url2"},{"api":"v1_0","url":"url3"},{"api":"v1_0","url":"url4"},{"api":"v1_0","url":"url5"}]', 'data: [{"api":"1_0","url":"too much versions at the same time test"},{"api":"1_0","url":"url2"},{"api":"1_0","url":"url3"},{"api":"1_0","url":"url4"},{"api":"1_0","url":"url5"}]',
) )
}) })
@ -266,17 +267,17 @@ describe('federation', () => {
jsonArray = [ jsonArray = [
{ {
wrong: 'wrong but tolerated property test', wrong: 'wrong but tolerated property test',
api: 'v1_0', api: '1_0',
url: 'url1', url: 'url1',
}, },
{ {
api: 'v2_0', api: '2_0',
url: 'url2', url: 'url2',
wrong: 'wrong but tolerated property test', wrong: 'wrong but tolerated property test',
}, },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -287,13 +288,14 @@ describe('federation', () => {
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
}) })
it('has an entry for api version v1_0', () => { it('has an entry for api version 1_0', () => {
expect(result).toEqual( expect(result).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v1_0', apiVersion: '1_0',
endPoint: 'url1', endPoint: 'url1',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
@ -303,13 +305,14 @@ describe('federation', () => {
) )
}) })
it('has an entry for api version v2_0', () => { it('has an entry for api version 2_0', () => {
expect(result).toEqual( expect(result).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v2_0', apiVersion: '2_0',
endPoint: 'url2', endPoint: 'url2',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
@ -535,7 +538,7 @@ describe('federation', () => {
{ api: 'toolong api', url: 'some valid url' }, { api: 'toolong api', url: 'some valid url' },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -551,6 +554,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api', apiVersion: 'valid api',
endPoint: endPoint:
@ -588,7 +592,7 @@ describe('federation', () => {
}, },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -604,6 +608,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api1', apiVersion: 'valid api1',
endPoint: endPoint:
@ -621,6 +626,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api2', apiVersion: 'valid api2',
endPoint: endPoint:
@ -638,6 +644,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api3', apiVersion: 'valid api3',
endPoint: endPoint:
@ -655,6 +662,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api4', apiVersion: 'valid api4',
endPoint: endPoint:
@ -710,17 +718,17 @@ describe('federation', () => {
Buffer.from( Buffer.from(
JSON.stringify([ JSON.stringify([
{ {
api: 'v1_0', api: '1_0',
url: 'http://localhost:4000/api/v1_0', url: 'http://localhost:5001/api/',
}, },
{ {
api: 'v2_0', api: '2_0',
url: 'http://localhost:4000/api/v2_0', url: 'http://localhost:5002/api/',
}, },
]), ]),
), ),
) )
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -736,9 +744,10 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v1_0', apiVersion: '1_0',
endPoint: 'http://localhost:4000/api/v1_0', endPoint: 'http://localhost:5001/api/',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
updatedAt: null, updatedAt: null,
@ -747,14 +756,15 @@ describe('federation', () => {
) )
}) })
it('has an entry for api version v2_0', () => { it('has an entry for api version 2_0', () => {
expect(result).toEqual( expect(result).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v2_0', apiVersion: '2_0',
endPoint: 'http://localhost:4000/api/v2_0', endPoint: 'http://localhost:5002/api/',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
updatedAt: null, updatedAt: null,
@ -775,16 +785,16 @@ describe('federation', () => {
Buffer.from( Buffer.from(
JSON.stringify([ JSON.stringify([
{ {
api: 'v1_0', api: '1_0',
url: 'http://localhost:4000/api/v1_0', url: 'http://localhost:5001/api/',
}, },
{ {
api: 'v1_1', api: '1_1',
url: 'http://localhost:4000/api/v1_1', url: 'http://localhost:5002/api/',
}, },
{ {
api: 'v2_0', api: '2_0',
url: 'http://localhost:4000/api/v2_0', url: 'http://localhost:5003/api/',
}, },
]), ]),
), ),

View File

@ -15,9 +15,9 @@ const ERRORTIME = 240000
const ANNOUNCETIME = 30000 const ANNOUNCETIME = 30000
enum ApiVersionType { enum ApiVersionType {
V1_0 = 'v1_0', V1_0 = '1_0',
V1_1 = 'v1_1', V1_1 = '1_1',
V2_0 = 'v2_0', V2_0 = '2_0',
} }
type CommunityApi = { type CommunityApi = {
api: string api: string
@ -31,13 +31,16 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey)
/*
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) { const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = { const comApi: CommunityApi = {
api: apiEnum, api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum, url: CONFIG.FEDERATION_COMMUNITY_URL,
} }
return comApi return comApi
}) })
*/
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`) logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -184,3 +187,35 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.error('DHT unexpected error:', err) logger.error('DHT unexpected error:', err)
} }
} }
async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const port =
Number.parseInt(CONFIG.FEDERATION_COMMUNITY_API_PORT) + Number(apiEnum.replace('_', ''))
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL + ':' + port.toString() + '/api/',
}
return comApi
})
try {
// first remove privious existing homeCommunity entries
DbCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
homeApiVersions.forEach(async function (homeApi) {
const homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.apiVersion = homeApi.api
homeCom.endPoint = homeApi.url
homeCom.publicKey = pubKey.toString('hex')
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbCommunity.insert(homeCom)
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`)
})
} catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
}
return homeApiVersions
}

View File

@ -11,7 +11,7 @@ Decimal.set({
*/ */
const constants = { const constants = {
DB_VERSION: '0059-add_hide_amount_to_users', DB_VERSION: '0060-update_communities_table',
// DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 // DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info

View File

@ -1,6 +1,6 @@
{ {
"name": "bootstrap-vue-gradido-wallet", "name": "bootstrap-vue-gradido-wallet",
"version": "1.18.1", "version": "1.18.2",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node run/server.js", "start": "node run/server.js",

View File

@ -1,5 +1,9 @@
<template> <template>
<div class="decayinformation-startblock"> <div class="decayinformation-startblock">
<div class="my-4">
<div class="font-weight-bold pb-2">{{ $t('form.memo') }}</div>
<div>{{ memo }}</div>
</div>
<div class="mt-3 mb-3 text-center"> <div class="mt-3 mb-3 text-center">
<b>{{ $t('decay.before_startblock_transaction') }}</b> <b>{{ $t('decay.before_startblock_transaction') }}</b>
</div> </div>
@ -8,5 +12,11 @@
<script> <script>
export default { export default {
name: 'DecayInformation-StartBlock', name: 'DecayInformation-StartBlock',
props: {
memo: {
type: String,
required: true,
},
},
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="decay-information-box"> <div class="decay-information-box">
<decay-information-before-startblock v-if="decay.start === null" /> <decay-information-before-startblock v-if="decay.start === null" :memo="memo" />
<decay-information-decay-startblock <decay-information-decay-startblock
v-else-if="isStartBlock" v-else-if="isStartBlock"
:amount="amount" :amount="amount"

View File

@ -179,6 +179,17 @@ export default {
}, },
}, },
computed: { computed: {
disabled() {
if (
this.form.email.length > 5 &&
parseInt(this.form.amount) <= parseInt(this.balance) &&
this.form.memo.length > 5 &&
this.form.memo.length <= 255
) {
return false
}
return true
},
isBalanceDisabled() { isBalanceDisabled() {
return this.balance <= 0 ? 'disabled' : false return this.balance <= 0 ? 'disabled' : false
}, },

View File

@ -3,20 +3,20 @@
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-6"> <div class="mb-2">
<h2>{{ $t('gdd_per_link.redeem') }}</h2> <h2>{{ $t('gdd_per_link.redeem') }}</h2>
</div> </div>
<b-row> <b-row>
<b-col col sm="12" md="6"> <b-col sm="12" md="6">
<p>{{ $t('gdd_per_link.no-account') }}</p> <p>{{ $t('gdd_per_link.no-account') }}</p>
<b-button variant="primary" :to="register"> <b-button variant="primary" :to="register">
{{ $t('gdd_per_link.to-register') }} {{ $t('gdd_per_link.to-register') }}
</b-button> </b-button>
</b-col> </b-col>
<b-col sm="12" md="6" class="mt-xs-6 mt-sm-6 mt-md-0"> <b-col sm="12" md="6" class="mt-4 mt-lg-0">
<p>{{ $t('gdd_per_link.has-account') }}</p> <p>{{ $t('gdd_per_link.has-account') }}</p>
<b-button variant="info" :to="login">{{ $t('gdd_per_link.to-login') }}</b-button> <b-button variant="gradido" :to="login">{{ $t('gdd_per_link.to-login') }}</b-button>
</b-col> </b-col>
</b-row> </b-row>
</b-jumbotron> </b-jumbotron>

View File

@ -3,7 +3,7 @@
<redeem-information v-bind="linkData" :isContributionLink="isContributionLink" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-3 text-center"> <div class="mb-3 text-center">
<b-button variant="primary" @click="$emit('mutation-link', linkData.amount)" size="lg"> <b-button variant="gradido" @click="$emit('mutation-link', linkData.amount)" size="lg">
{{ $t('gdd_per_link.redeem') }} {{ $t('gdd_per_link.redeem') }}
</b-button> </b-button>
</div> </div>

View File

@ -23,15 +23,17 @@
</b-col> </b-col>
</b-row> </b-row>
<b-row> <b-row>
<b-col class="d-flex justify-content-end"> <b-col class="d-flex justify-content-end mb-4 mb-lg-0">
<router-link to="/forgot-password" class="mt-3"> <router-link to="/forgot-password">
{{ $t('settings.password.forgot_pwd') }} {{ $t('settings.password.forgot_pwd') }}
</router-link> </router-link>
</b-col> </b-col>
</b-row> </b-row>
<div class="mt-5"> <b-row>
<b-button type="submit" variant="gradido">{{ $t('login') }}</b-button> <b-col cols="12" lg="4">
</div> <b-button type="submit" variant="gradido" block>{{ $t('login') }}</b-button>
</b-col>
</b-row>
</b-form> </b-form>
</validation-observer> </validation-observer>
</b-container> </b-container>

View File

@ -68,7 +68,8 @@
></input-email> ></input-email>
</b-col> </b-col>
</b-row> </b-row>
<div class="my-4"> <b-row>
<b-col cols="12" class="my-4">
<b-form-checkbox <b-form-checkbox
id="registerCheckbox" id="registerCheckbox"
v-model="form.agree" v-model="form.agree"
@ -77,16 +78,20 @@
<!-- eslint-disable-next-line @intlify/vue-i18n/no-v-html --> <!-- eslint-disable-next-line @intlify/vue-i18n/no-v-html -->
<span class="text-muted" v-html="$t('site.signup.agree')"></span> <span class="text-muted" v-html="$t('site.signup.agree')"></span>
</b-form-checkbox> </b-form-checkbox>
</div> </b-col>
<div> </b-row>
<b-row>
<b-col cols="12" lg="5">
<b-button <b-button
block
type="submit" type="submit"
:disabled="disabled" :disabled="disabled"
:variant="disabled ? 'gradido-disable' : 'gradido'" :variant="disabled ? 'gradido-disable' : 'gradido'"
> >
{{ $t('signup') }} {{ $t('signup') }}
</b-button> </b-button>
</div> </b-col>
</b-row>
</b-form> </b-form>
</validation-observer> </validation-observer>
</b-container> </b-container>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="show-transaction-link-informations"> <div class="show-transaction-link-informations">
<b-container class="mt-4"> <div class="mt-4">
<transaction-link-item :type="itemType"> <transaction-link-item :type="itemType">
<template #LOGGED_OUT> <template #LOGGED_OUT>
<redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" /> <redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" />
@ -22,7 +22,7 @@
<redeemed-text-box :text="redeemedBoxText" /> <redeemed-text-box :text="redeemedBoxText" />
</template> </template>
</transaction-link-item> </transaction-link-item>
</b-container> </div>
</div> </div>
</template> </template>
<script> <script>

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido", "name": "gradido",
"version": "1.18.1", "version": "1.18.2",
"description": "Gradido", "description": "Gradido",
"main": "index.js", "main": "index.js",
"repository": "git@github.com:gradido/gradido.git", "repository": "git@github.com:gradido/gradido.git",