diff --git a/CHANGELOG.md b/CHANGELOG.md
index 754566658..9ce354b1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,8 +4,44 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
+#### [1.14.1](https://github.com/gradido/gradido/compare/1.14.0...1.14.1)
+
+- fix(frontend): load contributionMessages is fixed [`#2390`](https://github.com/gradido/gradido/pull/2390)
+
+#### [1.14.0](https://github.com/gradido/gradido/compare/1.13.3...1.14.0)
+
+> 14 November 2022
+
+- chore(release): version 1.14.0 [`#2389`](https://github.com/gradido/gradido/pull/2389)
+- fix(frontend): close all open collapse by change tabs in community [`#2388`](https://github.com/gradido/gradido/pull/2388)
+- fix(backend): corrected E-Mail texts [`#2386`](https://github.com/gradido/gradido/pull/2386)
+- fix(frontend): better history messages [`#2381`](https://github.com/gradido/gradido/pull/2381)
+- fix(frontend): mailto link [`#2383`](https://github.com/gradido/gradido/pull/2383)
+- fix(admin): fix text in admin area to uppercase [`#2365`](https://github.com/gradido/gradido/pull/2365)
+- feat(frontend): move the information about gradido being free to the auth layout [`#2349`](https://github.com/gradido/gradido/pull/2349)
+- fix(admin): load error fixed for contribution link [`#2364`](https://github.com/gradido/gradido/pull/2364)
+- fix(admin): edit contribution link does not take old values [`#2362`](https://github.com/gradido/gradido/pull/2362)
+- fix(other): corrected dockerfile descriptions [`#2346`](https://github.com/gradido/gradido/pull/2346)
+- feat(backend): 🍰 Send email for rejected contributions [`#2340`](https://github.com/gradido/gradido/pull/2340)
+- feat(admin): edit automatic contribution link [`#2309`](https://github.com/gradido/gradido/pull/2309)
+- refactor(backend): fix logger mocks [`#2308`](https://github.com/gradido/gradido/pull/2308)
+- fix(admin): update contribution list after admin updates contribution [`#2330`](https://github.com/gradido/gradido/pull/2330)
+- fix(frontend): inconsistent labeling on login register [`#2350`](https://github.com/gradido/gradido/pull/2350)
+- feat(backend): setup hyperswarm [`#1874`](https://github.com/gradido/gradido/pull/1874)
+- feat(other): lint pull request workflow [`#2338`](https://github.com/gradido/gradido/pull/2338)
+- Feature: 🍰 add updated at to contributions [`#2237`](https://github.com/gradido/gradido/pull/2237)
+- Refactor: GitHub test workflow - disable video recording and reduce wait time [`#2336`](https://github.com/gradido/gradido/pull/2336)
+- 2274 feature concept manuel user registration for admins [`#2289`](https://github.com/gradido/gradido/pull/2289)
+- 1574 concept to introduce gradidoID and change password encryption [`#2252`](https://github.com/gradido/gradido/pull/2252)
+- contributionlink stage-2 and stage-3 of capturing and activation [`#2241`](https://github.com/gradido/gradido/pull/2241)
+- Github workflow: update actions to the current API version using Node v 16 [`#2323`](https://github.com/gradido/gradido/pull/2323)
+- feature: Fullstack tests in GitHub workflow [`#2319`](https://github.com/gradido/gradido/pull/2319)
+
#### [1.13.3](https://github.com/gradido/gradido/compare/1.13.2...1.13.3)
+> 1 November 2022
+
+- release: Version 1.13.3 [`#2322`](https://github.com/gradido/gradido/pull/2322)
- 2294 contribution links on its own page [`#2312`](https://github.com/gradido/gradido/pull/2312)
- fix: Change Orange Color [`#2302`](https://github.com/gradido/gradido/pull/2302)
- fix: Release Statistic Query Runner [`#2320`](https://github.com/gradido/gradido/pull/2320)
diff --git a/admin/package.json b/admin/package.json
index 82a2413de..7f0e7ffd5 100644
--- a/admin/package.json
+++ b/admin/package.json
@@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
- "version": "1.13.3",
+ "version": "1.14.1",
"license": "Apache-2.0",
"private": false,
"scripts": {
diff --git a/admin/src/components/CommunityStatistic.spec.js b/admin/src/components/CommunityStatistic.spec.js
deleted file mode 100644
index dbcca5fed..000000000
--- a/admin/src/components/CommunityStatistic.spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { mount } from '@vue/test-utils'
-import CommunityStatistic from './CommunityStatistic'
-
-const localVue = global.localVue
-
-const mocks = {
- $t: jest.fn((t) => t),
- $n: jest.fn((n) => n),
-}
-
-const propsData = {
- value: {
- totalUsers: '123',
- activeUsers: '100',
- deletedUsers: '5',
- totalGradidoCreated: '2500',
- totalGradidoDecayed: '200',
- totalGradidoAvailable: '500',
- totalGradidoUnbookedDecayed: '111',
- },
-}
-
-describe('CommunityStatistic', () => {
- let wrapper
-
- const Wrapper = () => {
- return mount(CommunityStatistic, { localVue, mocks, propsData })
- }
-
- describe('mount', () => {
- beforeEach(() => {
- wrapper = Wrapper()
- })
-
- it('renders the Div Element ".community-statistic"', () => {
- expect(wrapper.find('div.community-statistic').exists()).toBe(true)
- })
- })
-})
diff --git a/admin/src/components/CommunityStatistic.vue b/admin/src/components/CommunityStatistic.vue
deleted file mode 100644
index c19f8deec..000000000
--- a/admin/src/components/CommunityStatistic.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
diff --git a/admin/src/components/ContributionLink/ContributionLink.vue b/admin/src/components/ContributionLink/ContributionLink.vue
index c8963d3ab..ca82fcd42 100644
--- a/admin/src/components/ContributionLink/ContributionLink.vue
+++ b/admin/src/components/ContributionLink/ContributionLink.vue
@@ -34,6 +34,7 @@
:items="items"
@editContributionLinkData="editContributionLinkData"
@get-contribution-links="$emit('get-contribution-links')"
+ @closeContributionForm="closeContributionForm"
/>
{{ $t('contributionLink.noContributionLinks') }}
diff --git a/admin/src/components/ContributionLink/ContributionLinkForm.vue b/admin/src/components/ContributionLink/ContributionLinkForm.vue
index 85b9a3e95..2afd8e1c5 100644
--- a/admin/src/components/ContributionLink/ContributionLinkForm.vue
+++ b/admin/src/components/ContributionLink/ContributionLinkForm.vue
@@ -197,6 +197,7 @@ export default {
},
onReset() {
this.$refs.contributionLinkForm.reset()
+ this.form = {}
this.form.validFrom = null
this.form.validTo = null
},
diff --git a/admin/src/components/ContributionLink/ContributionLinkList.vue b/admin/src/components/ContributionLink/ContributionLinkList.vue
index c99821097..f67feced2 100644
--- a/admin/src/components/ContributionLink/ContributionLinkList.vue
+++ b/admin/src/components/ContributionLink/ContributionLinkList.vue
@@ -108,6 +108,7 @@ export default {
})
.then(() => {
this.toastSuccess(this.$t('contributionLink.deleted'))
+ this.$emit('closeContributionForm')
this.$emit('get-contribution-links')
})
.catch((err) => {
diff --git a/admin/src/components/ContributionMessages/LinkifyMessage.vue b/admin/src/components/ContributionMessages/ParseMessage.vue
similarity index 56%
rename from admin/src/components/ContributionMessages/LinkifyMessage.vue
rename to admin/src/components/ContributionMessages/ParseMessage.vue
index 71e30aece..069373840 100644
--- a/admin/src/components/ContributionMessages/LinkifyMessage.vue
+++ b/admin/src/components/ContributionMessages/ParseMessage.vue
@@ -1,7 +1,15 @@
-
+
{{ text }}
+
+ {{ $d(new Date(text), 'short') }}
+
+
+
+
+ {{ `${$n(Number(text), 'decimal')} GDD` }}
+
{{ text }}
@@ -12,17 +20,28 @@ const LINK_REGEX_PATTERN =
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*))/i
export default {
- name: 'LinkifyMessage',
+ name: 'ParseMessage',
props: {
message: {
type: String,
required: true,
},
+ type: {
+ type: String,
+ reuired: true,
+ },
},
computed: {
- linkifiedMessage() {
- const linkified = []
+ parsedMessage() {
let string = this.message
+ const linkified = []
+ let amount
+ if (this.type === 'HISTORY') {
+ const split = string.split(/\n\s*---\n\s*/)
+ string = split[1]
+ linkified.push({ type: 'date', text: split[0].trim() })
+ amount = split[2].trim()
+ }
let match
while ((match = string.match(LINK_REGEX_PATTERN))) {
if (match.index > 0)
@@ -31,6 +50,7 @@ export default {
string = string.substring(match.index + match[0].length)
}
if (string.length > 0) linkified.push({ type: 'text', text: string })
+ if (amount) linkified.push({ type: 'amount', text: amount })
return linkified
},
},
diff --git a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js
index c1a4e65c6..1b4f963e8 100644
--- a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js
+++ b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.spec.js
@@ -3,12 +3,16 @@ import ContributionMessagesListItem from './ContributionMessagesListItem.vue'
const localVue = global.localVue
+const dateMock = jest.fn((d) => d)
+const numberMock = jest.fn((n) => n)
+
describe('ContributionMessagesListItem', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
- $d: jest.fn((d) => d),
+ $d: dateMock,
+ $n: numberMock,
}
describe('if message author has moderator role', () => {
@@ -189,4 +193,64 @@ and here is the link to the repository: https://github.com/gradido/gradido`)
})
})
})
+
+ describe('contribution message type HISTORY', () => {
+ const propsData = {
+ message: {
+ id: 111,
+ message: `Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time)
+---
+This message also contains a link: https://gradido.net/de/
+---
+350.00`,
+ createdAt: '2022-08-29T12:23:27.000Z',
+ updatedAt: null,
+ type: 'HISTORY',
+ userFirstName: 'Peter',
+ userLastName: 'Lustig',
+ userId: 107,
+ __typename: 'ContributionMessage',
+ },
+ }
+
+ const itemWrapper = () => {
+ return mount(ContributionMessagesListItem, {
+ localVue,
+ mocks,
+ propsData,
+ })
+ }
+
+ let messageField
+
+ describe('render HISTORY message', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ wrapper = itemWrapper()
+ messageField = wrapper.find('div.is-not-moderator.text-left > div:nth-child(4)')
+ })
+
+ it('renders the date', () => {
+ expect(dateMock).toBeCalledWith(
+ new Date('Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time'),
+ 'short',
+ )
+ })
+
+ it('renders the amount', () => {
+ expect(numberMock).toBeCalledWith(350, 'decimal')
+ expect(messageField.text()).toContain('350 GDD')
+ })
+
+ it('contains the link as text', () => {
+ expect(messageField.text()).toContain(
+ 'This message also contains a link: https://gradido.net/de/',
+ )
+ })
+
+ it('contains a link to the given address', () => {
+ expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/')
+ })
+ })
+ })
})
diff --git a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.vue b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.vue
index 30960bd33..53006cff5 100644
--- a/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.vue
+++ b/admin/src/components/ContributionMessages/slots/ContributionMessagesListItem.vue
@@ -5,23 +5,23 @@
{{ message.userFirstName }} {{ message.userLastName }}
{{ $d(new Date(message.createdAt), 'short') }}
{{ $t('moderator') }}
-
+
{{ message.userFirstName }} {{ message.userLastName }}
{{ $d(new Date(message.createdAt), 'short') }}
-
+
diff --git a/admin/src/locales/de.json b/admin/src/locales/de.json
index c012e3171..ad7a668e2 100644
--- a/admin/src/locales/de.json
+++ b/admin/src/locales/de.json
@@ -85,7 +85,6 @@
"hide_details": "Details verbergen",
"lastname": "Nachname",
"math": {
- "colon": ":",
"equals": "=",
"exclaim": "!",
"pipe": "|",
@@ -98,12 +97,13 @@
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name",
"navbar": {
- "automaticContributions": "automatische Beiträge",
+ "automaticContributions": "Automatische Beiträge",
"logout": "Abmelden",
"multi_creation": "Mehrfachschöpfung",
"my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen",
"overview": "Übersicht",
+ "statistic": "Statistik",
"user_search": "Nutzersuche"
},
"not_open_creations": "Keine offenen Schöpfungen",
@@ -125,8 +125,9 @@
"save": "Speichern",
"statistic": {
"activeUsers": "Aktive Mitglieder",
+ "count": "Menge",
"deletedUsers": "Gelöschte Mitglieder",
- "name": "Statistik",
+ "details": "Details",
"totalGradidoAvailable": "GDD insgesamt im Umlauf",
"totalGradidoCreated": "GDD insgesamt geschöpft",
"totalGradidoDecayed": "GDD insgesamt verfallen",
diff --git a/admin/src/locales/en.json b/admin/src/locales/en.json
index 9bff733c5..3f8751fa1 100644
--- a/admin/src/locales/en.json
+++ b/admin/src/locales/en.json
@@ -85,7 +85,6 @@
"hide_details": "Hide details",
"lastname": "Lastname",
"math": {
- "colon": ":",
"equals": "=",
"exclaim": "!",
"pipe": "|",
@@ -104,6 +103,7 @@
"my-account": "My Account",
"open_creation": "Open creations",
"overview": "Overview",
+ "statistic": "Statistic",
"user_search": "User search"
},
"not_open_creations": "No open creations",
@@ -125,8 +125,9 @@
"save": "Speichern",
"statistic": {
"activeUsers": "Active members",
+ "count": "Count",
"deletedUsers": "Deleted members",
- "name": "Statistic",
+ "details": "Details",
"totalGradidoAvailable": "Total GDD in circulation",
"totalGradidoCreated": "Total created GDD",
"totalGradidoDecayed": "Total GDD decay",
diff --git a/admin/src/pages/CommunityStatistic.spec.js b/admin/src/pages/CommunityStatistic.spec.js
new file mode 100644
index 000000000..50e04d11f
--- /dev/null
+++ b/admin/src/pages/CommunityStatistic.spec.js
@@ -0,0 +1,98 @@
+import { mount } from '@vue/test-utils'
+import CommunityStatistic from './CommunityStatistic.vue'
+import { communityStatistics } from '@/graphql/communityStatistics.js'
+import { toastErrorSpy } from '../../test/testSetup'
+import VueApollo from 'vue-apollo'
+import { createMockClient } from 'mock-apollo-client'
+
+const mockClient = createMockClient()
+const apolloProvider = new VueApollo({
+ defaultClient: mockClient,
+})
+
+const localVue = global.localVue
+localVue.use(VueApollo)
+
+const defaultData = () => {
+ return {
+ communityStatistics: {
+ totalUsers: 3113,
+ activeUsers: 1057,
+ deletedUsers: 35,
+ totalGradidoCreated: '4083774.05000000000000000000',
+ totalGradidoDecayed: '-1062639.13634129622923372197',
+ totalGradidoAvailable: '2513565.869444365732411569',
+ totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
+ },
+ }
+}
+
+const mocks = {
+ $t: jest.fn((t) => t),
+ $n: jest.fn((n) => n),
+}
+
+describe('CommunityStatistic', () => {
+ let wrapper
+
+ const communityStatisticsMock = jest.fn()
+
+ mockClient.setRequestHandler(
+ communityStatistics,
+ communityStatisticsMock
+ .mockRejectedValueOnce({ message: 'Ouch!' })
+ .mockResolvedValue({ data: defaultData() }),
+ )
+
+ const Wrapper = () => {
+ return mount(CommunityStatistic, { localVue, mocks, apolloProvider })
+ }
+
+ describe('mount', () => {
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ it('renders the Div Element ".community-statistic"', () => {
+ expect(wrapper.find('div.community-statistic').exists()).toBe(true)
+ })
+
+ describe('server response for get statistics is an error', () => {
+ it('toast an error message', () => {
+ expect(toastErrorSpy).toBeCalledWith('Ouch!')
+ })
+ })
+
+ describe('server response for getting statistics is success', () => {
+ it('renders the data correctly', () => {
+ expect(wrapper.findAll('tr').at(1).findAll('td').at(1).text()).toEqual('3113')
+ expect(wrapper.findAll('tr').at(2).findAll('td').at(1).text()).toEqual('1057')
+ expect(wrapper.findAll('tr').at(3).findAll('td').at(1).text()).toEqual('35')
+ expect(wrapper.findAll('tr').at(4).findAll('td').at(1).text()).toEqual(
+ '4083774.05000000000000000000 GDD',
+ )
+ expect(wrapper.findAll('tr').at(4).findAll('td').at(2).text()).toEqual(
+ '4083774.05000000000000000000',
+ )
+ expect(wrapper.findAll('tr').at(5).findAll('td').at(1).text()).toEqual(
+ '-1062639.13634129622923372197 GDD',
+ )
+ expect(wrapper.findAll('tr').at(5).findAll('td').at(2).text()).toEqual(
+ '-1062639.13634129622923372197',
+ )
+ expect(wrapper.findAll('tr').at(6).findAll('td').at(1).text()).toEqual(
+ '2513565.869444365732411569 GDD',
+ )
+ expect(wrapper.findAll('tr').at(6).findAll('td').at(2).text()).toEqual(
+ '2513565.869444365732411569',
+ )
+ expect(wrapper.findAll('tr').at(7).findAll('td').at(1).text()).toEqual(
+ '-500474.6738366222166261272 GDD',
+ )
+ expect(wrapper.findAll('tr').at(7).findAll('td').at(2).text()).toEqual(
+ '-500474.6738366222166261272',
+ )
+ })
+ })
+ })
+})
diff --git a/admin/src/pages/CommunityStatistic.vue b/admin/src/pages/CommunityStatistic.vue
new file mode 100644
index 000000000..3b4865ee3
--- /dev/null
+++ b/admin/src/pages/CommunityStatistic.vue
@@ -0,0 +1,42 @@
+
+
+
+
diff --git a/admin/src/pages/Overview.spec.js b/admin/src/pages/Overview.spec.js
index affd018a7..8c714853f 100644
--- a/admin/src/pages/Overview.spec.js
+++ b/admin/src/pages/Overview.spec.js
@@ -1,6 +1,5 @@
import { mount } from '@vue/test-utils'
import Overview from './Overview.vue'
-import { communityStatistics } from '@/graphql/communityStatistics.js'
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
const localVue = global.localVue
@@ -22,19 +21,6 @@ const apolloQueryMock = jest
],
},
})
- .mockResolvedValueOnce({
- data: {
- communityStatistics: {
- totalUsers: 3113,
- activeUsers: 1057,
- deletedUsers: 35,
- totalGradidoCreated: '4083774.05000000000000000000',
- totalGradidoDecayed: '-1062639.13634129622923372197',
- totalGradidoAvailable: '2513565.869444365732411569',
- totalGradidoUnbookedDecayed: '-500474.6738366222166261272',
- },
- },
- })
.mockResolvedValue({
data: {
listUnconfirmedContributions: [
@@ -88,14 +74,6 @@ describe('Overview', () => {
)
})
- it('calls communityStatistics', () => {
- expect(apolloQueryMock).toBeCalledWith(
- expect.objectContaining({
- query: communityStatistics,
- }),
- )
- })
-
it('commits three pending creations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3)
})
diff --git a/admin/src/pages/Overview.vue b/admin/src/pages/Overview.vue
index 57bf7ff8c..115fffb72 100644
--- a/admin/src/pages/Overview.vue
+++ b/admin/src/pages/Overview.vue
@@ -28,32 +28,13 @@
-
diff --git a/admin/src/plugins/apolloProvider.js b/admin/src/plugins/apolloProvider.js
index 95b7aab7e..8b02013f4 100644
--- a/admin/src/plugins/apolloProvider.js
+++ b/admin/src/plugins/apolloProvider.js
@@ -10,7 +10,7 @@ const authLink = new ApolloLink((operation, forward) => {
operation.setContext({
headers: {
Authorization: token && token.length > 0 ? `Bearer ${token}` : '',
- clientRequestTime: new Date().toString(),
+ clientTimezoneOffset: new Date().getTimezoneOffset(),
},
})
return forward(operation).map((response) => {
diff --git a/admin/src/plugins/apolloProvider.test.js b/admin/src/plugins/apolloProvider.test.js
index 7889c3318..483862bea 100644
--- a/admin/src/plugins/apolloProvider.test.js
+++ b/admin/src/plugins/apolloProvider.test.js
@@ -94,7 +94,7 @@ describe('apolloProvider', () => {
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: 'Bearer some-token',
- clientRequestTime: expect.any(String),
+ clientTimezoneOffset: expect.any(Number),
},
})
})
@@ -110,7 +110,7 @@ describe('apolloProvider', () => {
expect(setContextMock).toBeCalledWith({
headers: {
Authorization: '',
- clientRequestTime: expect.any(String),
+ clientTimezoneOffset: expect.any(Number),
},
})
})
diff --git a/admin/src/router/router.test.js b/admin/src/router/router.test.js
index 22273c15b..fdc4b0b83 100644
--- a/admin/src/router/router.test.js
+++ b/admin/src/router/router.test.js
@@ -44,8 +44,8 @@ describe('router', () => {
})
describe('routes', () => {
- it('has seven routes defined', () => {
- expect(routes).toHaveLength(8)
+ it('has nine routes defined', () => {
+ expect(routes).toHaveLength(9)
})
it('has "/overview" as default', async () => {
@@ -82,12 +82,19 @@ describe('router', () => {
})
describe('contribution-links', () => {
- it('loads the "ContributionLinks" component', async () => {
+ it('loads the "ContributionLinks" page', async () => {
const component = await routes.find((r) => r.path === '/contribution-links').component()
expect(component.default.name).toBe('ContributionLinks')
})
})
+ describe('statistics', () => {
+ it('loads the "CommunityStatistic" page', async () => {
+ const component = await routes.find((r) => r.path === '/statistic').component()
+ expect(component.default.name).toBe('CommunityStatistic')
+ })
+ })
+
describe('not found page', () => {
it('renders the "NotFound" component', async () => {
const component = await routes.find((r) => r.path === '*').component()
diff --git a/admin/src/router/routes.js b/admin/src/router/routes.js
index ee82f128e..e365a6e40 100644
--- a/admin/src/router/routes.js
+++ b/admin/src/router/routes.js
@@ -6,6 +6,10 @@ const routes = [
path: '/',
component: () => import('@/pages/Overview.vue'),
},
+ {
+ path: '/statistic',
+ component: () => import('@/pages/CommunityStatistic.vue'),
+ },
{
// TODO: Implement a "You are logged out"-Page
path: '/logout',
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 035841c17..6225a4cd7 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,7 +1,7 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
-FROM node:12.19.0-alpine3.10 as base
+FROM node:18.7.0-alpine3.16 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
diff --git a/backend/package.json b/backend/package.json
index 1db683b2a..3e26225bf 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "gradido-backend",
- "version": "1.13.3",
+ "version": "1.14.1",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@@ -19,6 +19,8 @@
},
"dependencies": {
"@hyperswarm/dht": "^6.2.0",
+ "@types/email-templates": "^10.0.1",
+ "@types/i18n": "^0.13.4",
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/uuid": "^8.3.4",
@@ -30,14 +32,17 @@
"cross-env": "^7.0.3",
"decimal.js-light": "^2.5.1",
"dotenv": "^10.0.0",
+ "email-templates": "^10.0.1",
"express": "^4.17.1",
"graphql": "^15.5.1",
+ "i18n": "^0.15.1",
"jest": "^27.2.4",
"jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.4.6",
"mysql2": "^2.3.0",
"nodemailer": "^6.6.5",
+ "pug": "^3.0.2",
"random-bigint": "^0.0.1",
"reflect-metadata": "^0.1.13",
"sodium-native": "^3.3.0",
diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts
index e7139033b..26227b90d 100644
--- a/backend/src/config/index.ts
+++ b/backend/src/config/index.ts
@@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
- DB_VERSION: '0052-add_updated_at_to_contributions',
+ DB_VERSION: '0053-change_password_encryption',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
diff --git a/backend/src/emails/README.md b/backend/src/emails/README.md
new file mode 100644
index 000000000..9ab1d1124
--- /dev/null
+++ b/backend/src/emails/README.md
@@ -0,0 +1,50 @@
+# Using `forwardemail`–`email-templates` With `pug` Package
+
+You'll find the GitHub repository of the `email-templates` package and the `pug` package here:
+
+- [email-templates](https://github.com/forwardemail/email-templates)
+- [pug](https://www.npmjs.com/package/pug)
+
+## `pug` Documentation
+
+The full `pug` documentation you'll find here:
+
+- [pugjs.org](https://pugjs.org/)
+
+### Caching Possibility
+
+In case we are sending many emails in the future there is the possibility to cache the `pug` templates:
+
+- [cache-pug-templates](https://github.com/ladjs/cache-pug-templates)
+
+## Testing
+
+To test your send emails you have different possibilities:
+
+### In General
+
+To send emails to yourself while developing set in `.env` the value `EMAIL_TEST_MODUS=true` and `EMAIL_TEST_RECEIVER` to your preferred email address.
+
+### Unit Or Integration Tests
+
+To change the behavior to show previews etc. you have the following options to be set in `sendEmailTranslated.ts` on creating the email object:
+
+```js
+ const email = new Email({
+ …
+ // send emails in development/test env:
+ send: true,
+ …
+ // to open send emails in the browser
+ preview: true,
+ // or
+ // to open send emails in a specific the browser
+ preview: {
+ open: {
+ app: 'firefox',
+ wait: false,
+ },
+ },
+ …
+ })
+```
diff --git a/backend/src/emails/accountMultiRegistration/html.pug b/backend/src/emails/accountMultiRegistration/html.pug
new file mode 100644
index 000000000..4c8a94d28
--- /dev/null
+++ b/backend/src/emails/accountMultiRegistration/html.pug
@@ -0,0 +1,22 @@
+doctype html
+html(lang=locale)
+ head
+ title= t('emails.accountMultiRegistration.subject')
+ body
+ h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
+ #container.col
+ p(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.helloName', { firstName, lastName })
+ p= t('emails.accountMultiRegistration.emailReused')
+ br
+ span= t('emails.accountMultiRegistration.emailExists')
+ p= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
+ br
+ a(href=resendLink) #{resendLink}
+ br
+ span= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
+ p= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
+ br
+ a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/
+ p(style='margin-top: 24px;')= t('emails.accountMultiRegistration.sincerelyYours')
+ br
+ span= t('emails.accountMultiRegistration.yourGradidoTeam')
diff --git a/backend/src/emails/accountMultiRegistration/subject.pug b/backend/src/emails/accountMultiRegistration/subject.pug
new file mode 100644
index 000000000..322f07c78
--- /dev/null
+++ b/backend/src/emails/accountMultiRegistration/subject.pug
@@ -0,0 +1 @@
+= t('emails.accountMultiRegistration.subject')
\ No newline at end of file
diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts
new file mode 100644
index 000000000..28327f779
--- /dev/null
+++ b/backend/src/emails/sendEmailTranslated.test.ts
@@ -0,0 +1,110 @@
+import { createTransport } from 'nodemailer'
+import { logger, i18n } from '@test/testSetup'
+import CONFIG from '@/config'
+import { sendEmailTranslated } from './sendEmailTranslated'
+
+CONFIG.EMAIL = false
+CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
+CONFIG.EMAIL_SMTP_PORT = '1234'
+CONFIG.EMAIL_USERNAME = 'user'
+CONFIG.EMAIL_PASSWORD = 'pwd'
+
+jest.mock('nodemailer', () => {
+ return {
+ __esModule: true,
+ createTransport: jest.fn(() => {
+ return {
+ sendMail: jest.fn(() => {
+ return {
+ messageId: 'message',
+ }
+ }),
+ }
+ }),
+ }
+})
+
+describe('sendEmailTranslated', () => {
+ let result: Record | null
+
+ describe('config email is false', () => {
+ beforeEach(async () => {
+ result = await sendEmailTranslated({
+ receiver: {
+ to: 'receiver@mail.org',
+ cc: 'support@gradido.net',
+ },
+ template: 'accountMultiRegistration',
+ locals: {
+ locale: 'en',
+ },
+ })
+ })
+
+ it('logs warning', () => {
+ expect(logger.info).toBeCalledWith('Emails are disabled via config...')
+ })
+
+ it('returns false', () => {
+ expect(result).toBeFalsy()
+ })
+ })
+
+ describe('config email is true', () => {
+ beforeEach(async () => {
+ CONFIG.EMAIL = true
+ result = await sendEmailTranslated({
+ receiver: {
+ to: 'receiver@mail.org',
+ cc: 'support@gradido.net',
+ },
+ template: 'accountMultiRegistration',
+ locals: {
+ locale: 'en',
+ },
+ })
+ })
+
+ it('calls the transporter', () => {
+ expect(createTransport).toBeCalledWith({
+ host: 'EMAIL_SMTP_URL',
+ port: 1234,
+ secure: false,
+ requireTLS: true,
+ auth: {
+ user: 'user',
+ pass: 'pwd',
+ },
+ })
+ })
+
+ describe('call of "sendEmailTranslated"', () => {
+ it('has expected result', () => {
+ expect(result).toMatchObject({
+ envelope: {
+ from: 'info@gradido.net',
+ to: ['receiver@mail.org', 'support@gradido.net'],
+ },
+ message: expect.any(String),
+ originalMessage: expect.objectContaining({
+ to: 'receiver@mail.org',
+ cc: 'support@gradido.net',
+ from: 'Gradido (nicht antworten) ',
+ attachments: [],
+ subject: 'Gradido: Try To Register Again With Your Email',
+ html: expect.stringContaining('Gradido: Try To Register Again With Your Email'),
+ text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
+ }),
+ })
+ })
+ })
+
+ it.skip('calls "i18n.setLocale" with "en"', () => {
+ expect(i18n.setLocale).toBeCalledWith('en')
+ })
+
+ it.skip('calls "i18n.__" for translation', () => {
+ expect(i18n.__).toBeCalled()
+ })
+ })
+})
diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts
new file mode 100644
index 000000000..3fe4177f4
--- /dev/null
+++ b/backend/src/emails/sendEmailTranslated.ts
@@ -0,0 +1,85 @@
+import { backendLogger as logger } from '@/server/logger'
+import path from 'path'
+import { createTransport } from 'nodemailer'
+import Email from 'email-templates'
+import i18n from 'i18n'
+
+import CONFIG from '@/config'
+
+export const sendEmailTranslated = async (params: {
+ receiver: {
+ to: string
+ cc?: string
+ }
+ template: string
+ locals: Record
+}): Promise | null> => {
+ let resultSend: Record | null = null
+
+ // TODO: test the calling order of 'i18n.setLocale' for example: language of logging 'en', language of email receiver 'es', reset language of current user 'de'
+
+ // because language of receiver can differ from language of current user who triggers the sending
+ const rememberLocaleToRestore = i18n.getLocale()
+
+ i18n.setLocale('en') // for logging
+ logger.info(
+ `send Email: language=${params.locals.locale} to=${params.receiver.to}` +
+ (params.receiver.cc ? `, cc=${params.receiver.cc}` : '') +
+ `, subject=${i18n.__('emails.' + params.template + '.subject')}`,
+ )
+
+ if (!CONFIG.EMAIL) {
+ logger.info(`Emails are disabled via config...`)
+ return null
+ }
+ // because 'CONFIG.EMAIL_TEST_MODUS' can be boolean 'true' or string '`false`'
+ if (CONFIG.EMAIL_TEST_MODUS === true) {
+ logger.info(
+ `Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
+ )
+ params.receiver.to = CONFIG.EMAIL_TEST_RECEIVER
+ }
+ const transport = createTransport({
+ host: CONFIG.EMAIL_SMTP_URL,
+ port: Number(CONFIG.EMAIL_SMTP_PORT),
+ secure: false, // true for 465, false for other ports
+ requireTLS: true,
+ auth: {
+ user: CONFIG.EMAIL_USERNAME,
+ pass: CONFIG.EMAIL_PASSWORD,
+ },
+ })
+
+ i18n.setLocale(params.locals.locale) // for email
+
+ // TESTING: see 'README.md'
+ const email = new Email({
+ message: {
+ from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`,
+ },
+ transport,
+ preview: false,
+ // i18n, // is only needed if you don't install i18n
+ })
+
+ // ATTENTION: await is needed, because otherwise on send the email gets send in the language of the current user, because below the language gets reset
+ await email
+ .send({
+ template: path.join(__dirname, params.template),
+ message: params.receiver,
+ locals: params.locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
+ })
+ .then((result: Record) => {
+ resultSend = result
+ logger.info('Send email successfully !!!')
+ logger.info('Result: ', result)
+ })
+ .catch((error: unknown) => {
+ logger.error('Error sending notification email: ', error)
+ throw new Error('Error sending notification email!')
+ })
+
+ i18n.setLocale(rememberLocaleToRestore)
+
+ return resultSend
+}
diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts
new file mode 100644
index 000000000..4ac8221a7
--- /dev/null
+++ b/backend/src/emails/sendEmailVariants.test.ts
@@ -0,0 +1,88 @@
+import CONFIG from '@/config'
+import { sendAccountMultiRegistrationEmail } from './sendEmailVariants'
+import { sendEmailTranslated } from './sendEmailTranslated'
+
+CONFIG.EMAIL = true
+CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
+CONFIG.EMAIL_SMTP_PORT = '1234'
+CONFIG.EMAIL_USERNAME = 'user'
+CONFIG.EMAIL_PASSWORD = 'pwd'
+
+jest.mock('./sendEmailTranslated', () => {
+ const originalModule = jest.requireActual('./sendEmailTranslated')
+ return {
+ __esModule: true,
+ sendEmailTranslated: jest.fn((a) => originalModule.sendEmailTranslated(a)),
+ }
+})
+
+describe('sendEmailVariants', () => {
+ let result: Record | null
+
+ describe('sendAccountMultiRegistrationEmail', () => {
+ beforeAll(async () => {
+ result = await sendAccountMultiRegistrationEmail({
+ firstName: 'Peter',
+ lastName: 'Lustig',
+ email: 'peter@lustig.de',
+ language: 'en',
+ })
+ })
+
+ describe('calls "sendEmailTranslated"', () => {
+ it('with expected parameters', () => {
+ expect(sendEmailTranslated).toBeCalledWith({
+ receiver: {
+ to: 'Peter Lustig ',
+ },
+ template: 'accountMultiRegistration',
+ locals: {
+ firstName: 'Peter',
+ lastName: 'Lustig',
+ locale: 'en',
+ resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
+ },
+ })
+ })
+
+ it('has expected result', () => {
+ expect(result).toMatchObject({
+ envelope: {
+ from: 'info@gradido.net',
+ to: ['peter@lustig.de'],
+ },
+ message: expect.any(String),
+ originalMessage: expect.objectContaining({
+ to: 'Peter Lustig ',
+ from: 'Gradido (nicht antworten) ',
+ attachments: [],
+ subject: 'Gradido: Try To Register Again With Your Email',
+ html:
+ expect.stringContaining(
+ 'Gradido: Try To Register Again With Your Email',
+ ) &&
+ expect.stringContaining('>Gradido: Try To Register Again With Your Email') &&
+ expect.stringContaining(
+ 'Your email address has just been used again to register an account with Gradido.',
+ ) &&
+ expect.stringContaining(
+ 'However, an account already exists for your email address.',
+ ) &&
+ expect.stringContaining(
+ 'Please click on the following link if you have forgotten your password:',
+ ) &&
+ expect.stringContaining(
+ `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`,
+ ) &&
+ expect.stringContaining('or copy the link above into your browser window.') &&
+ expect.stringContaining(
+ 'If you are not the one who tried to register again, please contact our support:',
+ ) &&
+ expect.stringContaining('Sincerely yours,
your Gradido team'),
+ text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'),
+ }),
+ })
+ })
+ })
+ })
+})
diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts
new file mode 100644
index 000000000..fb142f206
--- /dev/null
+++ b/backend/src/emails/sendEmailVariants.ts
@@ -0,0 +1,20 @@
+import CONFIG from '@/config'
+import { sendEmailTranslated } from './sendEmailTranslated'
+
+export const sendAccountMultiRegistrationEmail = (data: {
+ firstName: string
+ lastName: string
+ email: string
+ language: string
+}): Promise | null> => {
+ return sendEmailTranslated({
+ receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
+ template: 'accountMultiRegistration',
+ locals: {
+ locale: data.language,
+ firstName: data.firstName,
+ lastName: data.lastName,
+ resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
+ },
+ })
+}
diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts
new file mode 100644
index 000000000..b3a00d748
--- /dev/null
+++ b/backend/src/graphql/enum/PasswordEncryptionType.ts
@@ -0,0 +1,12 @@
+import { registerEnumType } from 'type-graphql'
+
+export enum PasswordEncryptionType {
+ NO_PASSWORD = 0,
+ EMAIL = 1,
+ GRADIDO_ID = 2,
+}
+
+registerEnumType(PasswordEncryptionType, {
+ name: 'PasswordEncryptionType', // this one is mandatory
+ description: 'Type of the password encryption', // this one is optional
+})
diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts
index f26fce3d8..503bab472 100644
--- a/backend/src/graphql/resolver/AdminResolver.test.ts
+++ b/backend/src/graphql/resolver/AdminResolver.test.ts
@@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { objectValuesToArray } from '@/util/utilities'
-import { testEnvironment, resetToken, cleanDB } from '@test/helpers'
+import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index'
@@ -83,6 +83,12 @@ let user: User
let creation: Contribution | void
let result: any
+describe('contributionDateFormatter', () => {
+ it('formats the date correctly', () => {
+ expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024')
+ })
+})
+
describe('AdminResolver', () => {
describe('set user role', () => {
describe('unauthenticated', () => {
@@ -751,7 +757,7 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -861,7 +867,7 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -936,19 +942,25 @@ describe('AdminResolver', () => {
})
describe('adminCreateContribution', () => {
+ const now = new Date()
+
beforeAll(async () => {
- const now = new Date()
creation = await creationFactory(testEnv, {
email: 'peter@lustig.de',
amount: 400,
memo: 'Herzlich Willkommen bei Gradido!',
- creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(),
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, 1),
+ ),
})
})
describe('user to create for does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
+ variables.creationDate = contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, 1),
+ )
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@@ -969,6 +981,9 @@ describe('AdminResolver', () => {
beforeAll(async () => {
user = await userFactory(testEnv, stephenHawking)
variables.email = 'stephen@hawking.uk'
+ variables.creationDate = contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, 1),
+ )
})
it('throws an error', async () => {
@@ -995,6 +1010,9 @@ describe('AdminResolver', () => {
beforeAll(async () => {
user = await userFactory(testEnv, garrickOllivander)
variables.email = 'garrick@ollivander.com'
+ variables.creationDate = contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, 1),
+ )
})
it('throws an error', async () => {
@@ -1021,6 +1039,7 @@ describe('AdminResolver', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
variables.email = 'bibi@bloxberg.de'
+ variables.creationDate = 'invalid-date'
})
describe('date of creation is not a date string', () => {
@@ -1030,30 +1049,22 @@ describe('AdminResolver', () => {
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
- errors: [
- new GraphQLError('No information for available creations for the given date'),
- ],
+ errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)],
}),
)
})
it('logs the error thrown', () => {
- expect(logger.error).toBeCalledWith(
- 'No information for available creations with the given creationDate=',
- 'Invalid Date',
- )
+ expect(logger.error).toBeCalledWith(`invalid Date for creationDate=invalid-date`)
})
})
describe('date of creation is four months ago', () => {
it('throws an error', async () => {
jest.clearAllMocks()
- const now = new Date()
- variables.creationDate = new Date(
- now.getFullYear(),
- now.getMonth() - 4,
- 1,
- ).toString()
+ variables.creationDate = contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 4, 1),
+ )
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@@ -1068,7 +1079,7 @@ describe('AdminResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
- variables.creationDate,
+ new Date(variables.creationDate).toString(),
)
})
})
@@ -1076,12 +1087,9 @@ describe('AdminResolver', () => {
describe('date of creation is in the future', () => {
it('throws an error', async () => {
jest.clearAllMocks()
- const now = new Date()
- variables.creationDate = new Date(
- now.getFullYear(),
- now.getMonth() + 4,
- 1,
- ).toString()
+ variables.creationDate = contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() + 4, 1),
+ )
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@@ -1096,7 +1104,7 @@ describe('AdminResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
- variables.creationDate,
+ new Date(variables.creationDate).toString(),
)
})
})
@@ -1104,7 +1112,7 @@ describe('AdminResolver', () => {
describe('amount of creation is too high', () => {
it('throws an error', async () => {
jest.clearAllMocks()
- variables.creationDate = new Date().toString()
+ variables.creationDate = contributionDateFormatter(now)
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@@ -1192,7 +1200,7 @@ describe('AdminResolver', () => {
email,
amount: new Decimal(500),
memo: 'Grundeinkommen',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
}
})
@@ -1238,7 +1246,7 @@ describe('AdminResolver', () => {
email: 'bob@baumeister.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1268,7 +1276,7 @@ describe('AdminResolver', () => {
email: 'stephen@hawking.uk',
amount: new Decimal(300),
memo: 'Danke Bibi!',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1294,7 +1302,7 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1321,8 +1329,8 @@ describe('AdminResolver', () => {
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: creation
- ? creation.contributionDate.toString()
- : new Date().toString(),
+ ? contributionDateFormatter(creation.contributionDate)
+ : contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1356,8 +1364,8 @@ describe('AdminResolver', () => {
amount: new Decimal(1900),
memo: 'Danke Peter!',
creationDate: creation
- ? creation.contributionDate.toString()
- : new Date().toString(),
+ ? contributionDateFormatter(creation.contributionDate)
+ : contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1390,8 +1398,8 @@ describe('AdminResolver', () => {
amount: new Decimal(300),
memo: 'Danke Peter!',
creationDate: creation
- ? creation.contributionDate.toString()
- : new Date().toString(),
+ ? contributionDateFormatter(creation.contributionDate)
+ : contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1430,8 +1438,8 @@ describe('AdminResolver', () => {
amount: new Decimal(200),
memo: 'Das war leider zu Viel!',
creationDate: creation
- ? creation.contributionDate.toString()
- : new Date().toString(),
+ ? contributionDateFormatter(creation.contributionDate)
+ : contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@@ -1554,7 +1562,7 @@ describe('AdminResolver', () => {
variables: {
amount: 100.0,
memo: 'Test env contribution',
- creationDate: new Date().toString(),
+ creationDate: contributionDateFormatter(new Date()),
},
})
})
@@ -1633,7 +1641,9 @@ describe('AdminResolver', () => {
email: 'peter@lustig.de',
amount: 400,
memo: 'Herzlich Willkommen bei Gradido!',
- creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(),
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, 1),
+ ),
})
})
@@ -1664,7 +1674,9 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: 450,
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
- creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 2, 1),
+ ),
})
})
@@ -1735,13 +1747,17 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: 50,
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
- creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 2, 1),
+ ),
})
c2 = await creationFactory(testEnv, {
email: 'bibi@bloxberg.de',
amount: 50,
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
- creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 2, 1),
+ ),
})
})
diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts
index 479d020ea..80c69a864 100644
--- a/backend/src/graphql/resolver/AdminResolver.ts
+++ b/backend/src/graphql/resolver/AdminResolver.ts
@@ -1,4 +1,4 @@
-import { Context, getUser } from '@/server/context'
+import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { backendLogger as logger } from '@/server/logger'
import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type-graphql'
import {
@@ -49,6 +49,7 @@ import {
validateContribution,
isStartEndDateValid,
updateCreations,
+ isValidDateString,
} from './util/creations'
import {
CONTRIBUTIONLINK_NAME_MAX_CHARS,
@@ -86,7 +87,9 @@ export class AdminResolver {
async searchUsers(
@Args()
{ searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs,
+ @Ctx() context: Context,
): Promise {
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
const userRepository = getCustomRepository(UserRepository)
const userFields = [
'id',
@@ -114,7 +117,10 @@ export class AdminResolver {
}
}
- const creations = await getUserCreations(users.map((u) => u.id))
+ const creations = await getUserCreations(
+ users.map((u) => u.id),
+ clientTimezoneOffset,
+ )
const adminUsers = await Promise.all(
users.map(async (user) => {
@@ -237,6 +243,11 @@ export class AdminResolver {
logger.info(
`adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`,
)
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
+ if (!isValidDateString(creationDate)) {
+ logger.error(`invalid Date for creationDate=${creationDate}`)
+ throw new Error(`invalid Date for creationDate=${creationDate}`)
+ }
const emailContact = await UserContact.findOne({
where: { email },
withDeleted: true,
@@ -262,11 +273,11 @@ export class AdminResolver {
const event = new Event()
const moderator = getUser(context)
logger.trace('moderator: ', moderator.id)
- const creations = await getUserCreation(emailContact.userId)
+ const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset)
logger.trace('creations:', creations)
const creationDateObj = new Date(creationDate)
logger.trace('creationDateObj:', creationDateObj)
- validateContribution(creations, amount, creationDateObj)
+ validateContribution(creations, amount, creationDateObj, clientTimezoneOffset)
const contribution = DbContribution.create()
contribution.userId = emailContact.userId
contribution.amount = amount
@@ -289,7 +300,7 @@ export class AdminResolver {
event.setEventAdminContributionCreate(eventAdminCreateContribution),
)
- return getUserCreation(emailContact.userId)
+ return getUserCreation(emailContact.userId, clientTimezoneOffset)
}
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS])
@@ -325,6 +336,7 @@ export class AdminResolver {
@Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs,
@Ctx() context: Context,
): Promise {
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
const emailContact = await UserContact.findOne({
where: { email },
withDeleted: true,
@@ -365,17 +377,17 @@ export class AdminResolver {
}
const creationDateObj = new Date(creationDate)
- let creations = await getUserCreation(user.id)
+ let creations = await getUserCreation(user.id, clientTimezoneOffset)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
- creations = updateCreations(creations, contributionToUpdate)
+ creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
} else {
logger.error('Currently the month of the contribution cannot change.')
throw new Error('Currently the month of the contribution cannot change.')
}
// all possible cases not to be true are thrown in this function
- validateContribution(creations, amount, creationDateObj)
+ validateContribution(creations, amount, creationDateObj, clientTimezoneOffset)
contributionToUpdate.amount = amount
contributionToUpdate.memo = memo
contributionToUpdate.contributionDate = new Date(creationDate)
@@ -389,7 +401,7 @@ export class AdminResolver {
result.memo = contributionToUpdate.memo
result.date = contributionToUpdate.contributionDate
- result.creation = await getUserCreation(user.id)
+ result.creation = await getUserCreation(user.id, clientTimezoneOffset)
const event = new Event()
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
@@ -405,7 +417,8 @@ export class AdminResolver {
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => [UnconfirmedContribution])
- async listUnconfirmedContributions(): Promise {
+ async listUnconfirmedContributions(@Ctx() context: Context): Promise {
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
const contributions = await getConnection()
.createQueryBuilder()
.select('c')
@@ -419,7 +432,7 @@ export class AdminResolver {
}
const userIds = contributions.map((p) => p.userId)
- const userCreations = await getUserCreations(userIds)
+ const userCreations = await getUserCreations(userIds, clientTimezoneOffset)
const users = await dbUser.find({
where: { id: In(userIds) },
withDeleted: true,
@@ -493,6 +506,7 @@ export class AdminResolver {
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
): Promise {
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
const contribution = await DbContribution.findOne(id)
if (!contribution) {
logger.error(`Contribution not found for given id: ${id}`)
@@ -511,8 +525,13 @@ export class AdminResolver {
logger.error('This user was deleted. Cannot confirm a contribution.')
throw new Error('This user was deleted. Cannot confirm a contribution.')
}
- const creations = await getUserCreation(contribution.userId, false)
- validateContribution(creations, contribution.amount, contribution.contributionDate)
+ const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
+ validateContribution(
+ creations,
+ contribution.amount,
+ contribution.contributionDate,
+ clientTimezoneOffset,
+ )
const receivedCallDate = new Date()
diff --git a/backend/src/graphql/resolver/ContributionResolver.ts b/backend/src/graphql/resolver/ContributionResolver.ts
index a061304b7..15bdbfc2e 100644
--- a/backend/src/graphql/resolver/ContributionResolver.ts
+++ b/backend/src/graphql/resolver/ContributionResolver.ts
@@ -1,5 +1,5 @@
import { RIGHTS } from '@/auth/RIGHTS'
-import { Context, getUser } from '@/server/context'
+import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { backendLogger as logger } from '@/server/logger'
import { Contribution as dbContribution } from '@entity/Contribution'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
@@ -31,6 +31,7 @@ export class ContributionResolver {
@Args() { amount, memo, creationDate }: ContributionArgs,
@Ctx() context: Context,
): Promise {
+ 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)`)
@@ -44,10 +45,10 @@ export class ContributionResolver {
const event = new Event()
const user = getUser(context)
- const creations = await getUserCreation(user.id)
+ const creations = await getUserCreation(user.id, clientTimezoneOffset)
logger.trace('creations', creations)
const creationDateObj = new Date(creationDate)
- validateContribution(creations, amount, creationDateObj)
+ validateContribution(creations, amount, creationDateObj, clientTimezoneOffset)
const contribution = dbContribution.create()
contribution.userId = user.id
@@ -171,6 +172,7 @@ export class ContributionResolver {
@Args() { amount, memo, creationDate }: ContributionArgs,
@Ctx() context: Context,
): Promise {
+ 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)`)
@@ -206,16 +208,16 @@ export class ContributionResolver {
)
}
const creationDateObj = new Date(creationDate)
- let creations = await getUserCreation(user.id)
+ let creations = await getUserCreation(user.id, clientTimezoneOffset)
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
- creations = updateCreations(creations, contributionToUpdate)
+ creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
} else {
logger.error('Currently the month of the contribution cannot change.')
throw new Error('Currently the month of the contribution cannot change.')
}
// all possible cases not to be true are thrown in this function
- validateContribution(creations, amount, creationDateObj)
+ validateContribution(creations, amount, creationDateObj, clientTimezoneOffset)
const contributionMessage = ContributionMessage.create()
contributionMessage.contributionId = contributionId
diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts
index 74c531c54..a5c4a5f01 100644
--- a/backend/src/graphql/resolver/TransactionLinkResolver.ts
+++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts
@@ -1,5 +1,5 @@
import { backendLogger as logger } from '@/server/logger'
-import { Context, getUser } from '@/server/context'
+import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { getConnection } from '@dbTools/typeorm'
import {
Resolver,
@@ -169,6 +169,7 @@ export class TransactionLinkResolver {
@Arg('code', () => String) code: string,
@Ctx() context: Context,
): Promise {
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
const user = getUser(context)
const now = new Date()
@@ -258,9 +259,9 @@ export class TransactionLinkResolver {
}
}
- const creations = await getUserCreation(user.id)
+ const creations = await getUserCreation(user.id, clientTimezoneOffset)
logger.info('open creations', creations)
- validateContribution(creations, contributionLink.amount, now)
+ validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset)
const contribution = new DbContribution()
contribution.userId = user.id
contribution.createdAt = now
diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts
index d72223b1b..d8472fba9 100644
--- a/backend/src/graphql/resolver/UserResolver.test.ts
+++ b/backend/src/graphql/resolver/UserResolver.test.ts
@@ -19,7 +19,7 @@ import { GraphQLError } from 'graphql'
import { User } from '@entity/User'
import CONFIG from '@/config'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
-import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
+import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail'
import { printTimeDuration, activationLink } from './UserResolver'
import { contributionLinkFactory } from '@/seeds/factory/contributionLink'
@@ -29,13 +29,16 @@ import { TransactionLink } from '@entity/TransactionLink'
import { EventProtocolType } from '@/event/EventProtocolType'
import { EventProtocol } from '@entity/EventProtocol'
-import { logger } from '@test/testSetup'
+import { logger, i18n as localization } from '@test/testSetup'
import { validate as validateUUID, version as versionUUID } from 'uuid'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { UserContact } from '@entity/UserContact'
import { OptInType } from '../enum/OptInType'
import { UserContactType } from '../enum/UserContactType'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
+import { encryptPassword } from '@/password/PasswordEncryptor'
+import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
+import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
// import { klicktippSignIn } from '@/apis/KlicktippController'
@@ -46,7 +49,7 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => {
}
})
-jest.mock('@/mailer/sendAccountMultiRegistrationEmail', () => {
+jest.mock('@/emails/sendEmailVariants', () => {
return {
__esModule: true,
sendAccountMultiRegistrationEmail: jest.fn(),
@@ -73,7 +76,7 @@ let mutate: any, query: any, con: any
let testEnv: any
beforeAll(async () => {
- testEnv = await testEnvironment(logger)
+ testEnv = await testEnvironment(logger, localization)
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
@@ -146,6 +149,7 @@ describe('UserResolver', () => {
publisherId: 1234,
referrerId: null,
contributionLinkId: null,
+ passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
},
])
const valUUID = validateUUID(user[0].gradidoID)
@@ -213,6 +217,7 @@ describe('UserResolver', () => {
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
+ language: 'de',
})
})
@@ -490,7 +495,8 @@ describe('UserResolver', () => {
})
it('updates the password', () => {
- expect(newUser.password).toEqual('3917921995996627700')
+ const encryptedPass = encryptPassword(newUser, 'Aa12345_')
+ expect(newUser.password.toString()).toEqual(encryptedPass.toString())
})
/*
@@ -1158,6 +1164,93 @@ describe('UserResolver', () => {
})
})
})
+
+ describe('password encryption type', () => {
+ describe('user just registered', () => {
+ let bibi: User
+
+ it('has password type gradido id', async () => {
+ const users = await User.find()
+ bibi = users[1]
+
+ expect(bibi).toEqual(
+ expect.objectContaining({
+ password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
+ .readBigUInt64LE()
+ .toString(),
+ passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
+ }),
+ )
+ })
+ })
+
+ describe('user has encryption type email', () => {
+ const variables = {
+ email: 'bibi@bloxberg.de',
+ password: 'Aa12345_',
+ publisherId: 1234,
+ }
+
+ let bibi: User
+ beforeAll(async () => {
+ const usercontact = await UserContact.findOneOrFail(
+ { email: 'bibi@bloxberg.de' },
+ { relations: ['user'] },
+ )
+ bibi = usercontact.user
+ bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL
+ bibi.password = SecretKeyCryptographyCreateKey(
+ 'bibi@bloxberg.de',
+ 'Aa12345_',
+ )[0].readBigUInt64LE()
+
+ await bibi.save()
+ })
+
+ it('changes to gradidoID on login', async () => {
+ await mutate({ mutation: login, variables: variables })
+
+ const usercontact = await UserContact.findOneOrFail(
+ { email: 'bibi@bloxberg.de' },
+ { relations: ['user'] },
+ )
+ bibi = usercontact.user
+
+ expect(bibi).toEqual(
+ expect.objectContaining({
+ firstName: 'Bibi',
+ password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0]
+ .readBigUInt64LE()
+ .toString(),
+ passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID,
+ }),
+ )
+ })
+
+ it('can login after password change', async () => {
+ resetToken()
+ expect(await mutate({ mutation: login, variables: variables })).toEqual(
+ expect.objectContaining({
+ data: {
+ login: {
+ email: 'bibi@bloxberg.de',
+ firstName: 'Bibi',
+ hasElopage: false,
+ id: expect.any(Number),
+ isAdmin: null,
+ klickTipp: {
+ newsletterState: false,
+ },
+ language: 'de',
+ lastName: 'Bloxberg',
+ publisherId: 1234,
+ },
+ },
+ }),
+ )
+ })
+ })
+ })
})
describe('printTimeDuration', () => {
diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts
index 2287ede98..752c585fd 100644
--- a/backend/src/graphql/resolver/UserResolver.ts
+++ b/backend/src/graphql/resolver/UserResolver.ts
@@ -1,6 +1,7 @@
import fs from 'fs'
import { backendLogger as logger } from '@/server/logger'
-import { Context, getUser } from '@/server/context'
+import i18n from 'i18n'
+import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql'
import { getConnection, getCustomRepository, IsNull, Not } from '@dbTools/typeorm'
import CONFIG from '@/config'
@@ -18,7 +19,7 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
import { OptInType } from '@enum/OptInType'
import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail'
import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail'
-import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail'
+import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants'
import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys'
@@ -39,17 +40,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser'
import Paginated from '@arg/Paginated'
import { Order } from '@enum/Order'
import { v4 as uuidv4 } from 'uuid'
+import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils'
+import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
+import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
// eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const random = require('random-bigint')
-// We will reuse this for changePassword
-const isPassword = (password: string): boolean => {
- return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
-}
-
const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl']
const DEFAULT_LANGUAGE = 'de'
const isLanguage = (language: string): boolean => {
@@ -106,48 +105,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => {
return [pubKey, privKey]
}
-const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
- logger.trace('SecretKeyCryptographyCreateKey...')
- const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
- const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
- if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
- logger.error(
- `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
- )
- throw new Error(
- `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
- )
- }
-
- const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
- sodium.crypto_hash_sha512_init(state)
- sodium.crypto_hash_sha512_update(state, Buffer.from(salt))
- sodium.crypto_hash_sha512_update(state, configLoginAppSecret)
- const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
- sodium.crypto_hash_sha512_final(state, hash)
-
- const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES)
- const opsLimit = 10
- const memLimit = 33554432
- const algo = 2
- sodium.crypto_pwhash(
- encryptionKey,
- Buffer.from(password),
- hash.slice(0, sodium.crypto_pwhash_SALTBYTES),
- opsLimit,
- memLimit,
- algo,
- )
-
- const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
- sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
-
- logger.debug(
- `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`,
- )
- return [encryptionKeyHash, encryptionKey]
-}
-
/*
const getEmailHash = (email: string): Buffer => {
logger.trace('getEmailHash...')
@@ -305,8 +262,9 @@ export class UserResolver {
async verifyLogin(@Ctx() context: Context): Promise {
logger.info('verifyLogin...')
// TODO refactor and do not have duplicate code with login(see below)
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
const userEntity = getUser(context)
- const user = new User(userEntity, await getUserCreation(userEntity.id))
+ const user = new User(userEntity, await getUserCreation(userEntity.id, clientTimezoneOffset))
// user.pubkey = userEntity.pubKey.toString('hex')
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage(context)
@@ -323,6 +281,7 @@ export class UserResolver {
@Ctx() context: Context,
): Promise {
logger.info(`login with ${email}, ***, ${publisherId} ...`)
+ const clientTimezoneOffset = getClientTimezoneOffset(context)
email = email.trim().toLowerCase()
const dbUser = await findUserByEmail(email)
if (dbUser.deletedAt) {
@@ -343,19 +302,26 @@ export class UserResolver {
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
throw new Error('User has no private or publicKey')
}
- const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash
- const loginUserPassword = BigInt(dbUser.password.toString())
- if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) {
+
+ if (!verifyPassword(dbUser, password)) {
logger.error('The User has no valid credentials.')
throw new Error('No user with this credentials')
}
+
+ if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
+ dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
+ dbUser.password = encryptPassword(dbUser, password)
+ await dbUser.save()
+ }
// add pubKey in logger-context for layout-pattern X{user} to print it in each logging message
logger.addContext('user', dbUser.id)
logger.debug('validation of login credentials successful...')
- const user = new User(dbUser, await getUserCreation(dbUser.id))
+ const user = new User(dbUser, await getUserCreation(dbUser.id, clientTimezoneOffset))
logger.debug(`user= ${JSON.stringify(user, null, 2)}`)
+ i18n.setLocale(user.language)
+
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
logger.info('user.hasElopage=' + user.hasElopage)
@@ -408,6 +374,7 @@ export class UserResolver {
if (!language || !isLanguage(language)) {
language = DEFAULT_LANGUAGE
}
+ i18n.setLocale(language)
// check if user with email still exists?
email = email.trim().toLowerCase()
@@ -416,8 +383,11 @@ export class UserResolver {
logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`)
if (foundUser) {
- // ATTENTION: this logger-message will be exactly expected during tests
+ // ATTENTION: this logger-message will be exactly expected during tests, next line
logger.info(`User already exists with this email=${email}`)
+ logger.info(
+ `Specified username when trying to register multiple times with this email: firstName=${firstName}, lastName=${lastName}`,
+ )
// TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent.
const user = new User(communityDbUser)
@@ -430,18 +400,20 @@ export class UserResolver {
user.publisherId = publisherId
logger.debug('partly faked user=' + user)
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountMultiRegistrationEmail({
- firstName,
- lastName,
+ firstName: foundUser.firstName, // this is the real name of the email owner, but just "firstName" would be the name of the new registrant which shall not be passed to the outside
+ lastName: foundUser.lastName, // this is the real name of the email owner, but just "lastName" would be the name of the new registrant which shall not be passed to the outside
email,
+ language: foundUser.language, // use language of the emails owner for sending
})
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
)
- logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`)
+ logger.info(
+ `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
+ )
/* uncomment this, when you need the activation link on the console */
// In case EMails are disabled log the activation link for the user
if (!emailSent) {
@@ -470,6 +442,7 @@ export class UserResolver {
dbUser.lastName = lastName
dbUser.language = language
dbUser.publisherId = publisherId
+ dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
dbUser.passphrase = passphrase.join(' ')
logger.debug('new dbUser=' + dbUser)
if (redeemCode) {
@@ -623,7 +596,7 @@ export class UserResolver {
): Promise {
logger.info(`setPassword(${code}, ***)...`)
// Validate Password
- if (!isPassword(password)) {
+ if (!isValidPassword(password)) {
logger.error('Password entered is lexically invalid')
throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
@@ -681,10 +654,11 @@ export class UserResolver {
userContact.emailChecked = true
// Update Password
+ user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash
const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key
const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1])
- user.password = passwordHash[0].readBigUInt64LE() // using the shorthash
+ user.password = encryptPassword(user, password)
user.pubKey = keyPair[0]
user.privKey = encryptedPrivkey
logger.debug('User credentials updated ...')
@@ -785,11 +759,12 @@ export class UserResolver {
throw new Error(`"${language}" isn't a valid language`)
}
userEntity.language = language
+ i18n.setLocale(language)
}
if (password && passwordNew) {
// Validate Password
- if (!isPassword(passwordNew)) {
+ if (!isValidPassword(passwordNew)) {
logger.error('newPassword does not fullfil the rules')
throw new Error(
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
@@ -801,7 +776,7 @@ export class UserResolver {
userEntity.emailContact.email,
password,
)
- if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) {
+ if (!verifyPassword(userEntity, password)) {
logger.error(`Old password is invalid`)
throw new Error(`Old password is invalid`)
}
@@ -817,7 +792,8 @@ export class UserResolver {
logger.debug('PrivateKey encrypted...')
// Save new password hash and newly encrypted private key
- userEntity.password = newPasswordHash[0].readBigUInt64LE()
+ userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID
+ userEntity.password = encryptPassword(userEntity, passwordNew)
userEntity.privKey = encryptedPrivkey
}
diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts
new file mode 100644
index 000000000..8d747e989
--- /dev/null
+++ b/backend/src/graphql/resolver/util/creations.test.ts
@@ -0,0 +1,266 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+
+import { testEnvironment, cleanDB, contributionDateFormatter } from '@test/helpers'
+import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
+import { peterLustig } from '@/seeds/users/peter-lustig'
+import { User } from '@entity/User'
+import { Contribution } from '@entity/Contribution'
+import { userFactory } from '@/seeds/factory/user'
+import { login, createContribution, adminCreateContribution } from '@/seeds/graphql/mutations'
+import { getUserCreation } from './creations'
+
+let mutate: any, con: any
+let testEnv: any
+
+beforeAll(async () => {
+ testEnv = await testEnvironment()
+ mutate = testEnv.mutate
+ con = testEnv.con
+ await cleanDB()
+})
+
+afterAll(async () => {
+ await cleanDB()
+ await con.close()
+})
+
+const setZeroHours = (date: Date): Date => {
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate())
+}
+
+describe('util/creation', () => {
+ let user: User
+ let admin: User
+
+ const now = new Date()
+
+ beforeAll(async () => {
+ user = await userFactory(testEnv, bibiBloxberg)
+ admin = await userFactory(testEnv, peterLustig)
+ })
+
+ describe('getUserCreations', () => {
+ beforeAll(async () => {
+ await mutate({
+ mutation: login,
+ variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
+ })
+ await mutate({
+ mutation: adminCreateContribution,
+ variables: {
+ email: 'bibi@bloxberg.de',
+ amount: 250.0,
+ memo: 'Admin contribution for this month',
+ creationDate: contributionDateFormatter(now),
+ },
+ })
+ await mutate({
+ mutation: adminCreateContribution,
+ variables: {
+ email: 'bibi@bloxberg.de',
+ amount: 160.0,
+ memo: 'Admin contribution for the last month',
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
+ ),
+ },
+ })
+ await mutate({
+ mutation: adminCreateContribution,
+ variables: {
+ email: 'bibi@bloxberg.de',
+ amount: 450.0,
+ memo: 'Admin contribution for two months ago',
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()),
+ ),
+ },
+ })
+ await mutate({
+ mutation: login,
+ variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
+ })
+ await mutate({
+ mutation: createContribution,
+ variables: {
+ amount: 400.0,
+ memo: 'Contribution for this month',
+ creationDate: contributionDateFormatter(now),
+ },
+ })
+ await mutate({
+ mutation: createContribution,
+ variables: {
+ amount: 500.0,
+ memo: 'Contribution for the last month',
+ creationDate: contributionDateFormatter(
+ new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
+ ),
+ },
+ })
+ })
+
+ it('has the correct data setup', async () => {
+ await expect(Contribution.find()).resolves.toEqual([
+ expect.objectContaining({
+ userId: user.id,
+ contributionDate: setZeroHours(now),
+ amount: expect.decimalEqual(250),
+ memo: 'Admin contribution for this month',
+ moderatorId: admin.id,
+ contributionType: 'ADMIN',
+ contributionStatus: 'PENDING',
+ }),
+ expect.objectContaining({
+ userId: user.id,
+ contributionDate: setZeroHours(
+ new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
+ ),
+ amount: expect.decimalEqual(160),
+ memo: 'Admin contribution for the last month',
+ moderatorId: admin.id,
+ contributionType: 'ADMIN',
+ contributionStatus: 'PENDING',
+ }),
+ expect.objectContaining({
+ userId: user.id,
+ contributionDate: setZeroHours(
+ new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()),
+ ),
+ amount: expect.decimalEqual(450),
+ memo: 'Admin contribution for two months ago',
+ moderatorId: admin.id,
+ contributionType: 'ADMIN',
+ contributionStatus: 'PENDING',
+ }),
+ expect.objectContaining({
+ userId: user.id,
+ contributionDate: setZeroHours(now),
+ amount: expect.decimalEqual(400),
+ memo: 'Contribution for this month',
+ moderatorId: null,
+ contributionType: 'USER',
+ contributionStatus: 'PENDING',
+ }),
+ expect.objectContaining({
+ userId: user.id,
+ contributionDate: setZeroHours(
+ new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
+ ),
+ amount: expect.decimalEqual(500),
+ memo: 'Contribution for the last month',
+ moderatorId: null,
+ contributionType: 'USER',
+ contributionStatus: 'PENDING',
+ }),
+ ])
+ })
+
+ describe('call getUserCreation now', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, 0)).resolves.toEqual([
+ expect.decimalEqual(550),
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ ])
+ })
+
+ describe('run forward in time one hour before next month', () => {
+ const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0)
+
+ beforeAll(() => {
+ jest.useFakeTimers()
+ setTimeout(jest.fn(), targetDate.getTime() - now.getTime())
+ jest.runAllTimers()
+ })
+
+ afterAll(() => {
+ jest.useRealTimers()
+ })
+
+ it('has the clock set correctly', () => {
+ expect(new Date().toISOString()).toContain(
+ `${targetDate.getFullYear()}-${targetDate.getMonth() + 1}-${targetDate.getDate()}T23:`,
+ )
+ })
+
+ describe('call getUserCreation with UTC', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, 0)).resolves.toEqual([
+ expect.decimalEqual(550),
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ ])
+ })
+ })
+
+ describe('call getUserCreation with JST (GMT+0900)', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, -540, true)).resolves.toEqual([
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ expect.decimalEqual(1000),
+ ])
+ })
+ })
+
+ describe('call getUserCreation with PST (GMT-0800)', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, 480, true)).resolves.toEqual([
+ expect.decimalEqual(550),
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ ])
+ })
+ })
+
+ describe('run two hours forward to be in the next month in UTC', () => {
+ const nextMonthTargetDate = new Date()
+ nextMonthTargetDate.setTime(targetDate.getTime() + 2 * 60 * 60 * 1000)
+
+ beforeAll(() => {
+ setTimeout(jest.fn(), 2 * 60 * 60 * 1000)
+ jest.runAllTimers()
+ })
+
+ it('has the clock set correctly', () => {
+ expect(new Date().toISOString()).toContain(
+ `${nextMonthTargetDate.getFullYear()}-${nextMonthTargetDate.getMonth() + 1}-01T01:`,
+ )
+ })
+
+ describe('call getUserCreation with UTC', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, 0, true)).resolves.toEqual([
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ expect.decimalEqual(1000),
+ ])
+ })
+ })
+
+ describe('call getUserCreation with JST (GMT+0900)', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, -540, true)).resolves.toEqual([
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ expect.decimalEqual(1000),
+ ])
+ })
+ })
+
+ describe('call getUserCreation with PST (GMT-0800)', () => {
+ it('returns the expected open contributions', async () => {
+ await expect(getUserCreation(user.id, 450, true)).resolves.toEqual([
+ expect.decimalEqual(550),
+ expect.decimalEqual(340),
+ expect.decimalEqual(350),
+ ])
+ })
+ })
+ })
+ })
+ })
+ })
+})
diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts
index abf4017cb..eb4b6394d 100644
--- a/backend/src/graphql/resolver/util/creations.ts
+++ b/backend/src/graphql/resolver/util/creations.ts
@@ -13,9 +13,10 @@ export const validateContribution = (
creations: Decimal[],
amount: Decimal,
creationDate: Date,
+ timezoneOffset: number,
): void => {
logger.trace('isContributionValid: ', creations, amount, creationDate)
- const index = getCreationIndex(creationDate.getMonth())
+ const index = getCreationIndex(creationDate.getMonth(), timezoneOffset)
if (index < 0) {
logger.error(
@@ -37,10 +38,11 @@ export const validateContribution = (
export const getUserCreations = async (
ids: number[],
+ timezoneOffset: number,
includePending = true,
): Promise => {
logger.trace('getUserCreations:', ids, includePending)
- const months = getCreationMonths()
+ const months = getCreationMonths(timezoneOffset)
logger.trace('getUserCreations months', months)
const queryRunner = getConnection().createQueryRunner()
@@ -87,24 +89,29 @@ export const getUserCreations = async (
})
}
-export const getUserCreation = async (id: number, includePending = true): Promise => {
- logger.trace('getUserCreation', id, includePending)
- const creations = await getUserCreations([id], includePending)
+export const getUserCreation = async (
+ id: number,
+ timezoneOffset: number,
+ includePending = true,
+): Promise => {
+ logger.trace('getUserCreation', id, includePending, timezoneOffset)
+ const creations = await getUserCreations([id], timezoneOffset, includePending)
logger.trace('getUserCreation creations=', creations)
return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE
}
-export const getCreationMonths = (): number[] => {
- const now = new Date(Date.now())
+const getCreationMonths = (timezoneOffset: number): number[] => {
+ const clientNow = new Date()
+ clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
return [
- now.getMonth() + 1,
- new Date(now.getFullYear(), now.getMonth() - 1, 1).getMonth() + 1,
- new Date(now.getFullYear(), now.getMonth() - 2, 1).getMonth() + 1,
- ].reverse()
+ new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1,
+ new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1,
+ clientNow.getMonth() + 1,
+ ]
}
-export const getCreationIndex = (month: number): number => {
- return getCreationMonths().findIndex((el) => el === month + 1)
+const getCreationIndex = (month: number, timezoneOffset: number): number => {
+ return getCreationMonths(timezoneOffset).findIndex((el) => el === month + 1)
}
export const isStartEndDateValid = (
@@ -128,8 +135,12 @@ export const isStartEndDateValid = (
}
}
-export const updateCreations = (creations: Decimal[], contribution: Contribution): Decimal[] => {
- const index = getCreationIndex(contribution.contributionDate.getMonth())
+export const updateCreations = (
+ creations: Decimal[],
+ contribution: Contribution,
+ timezoneOffset: number,
+): Decimal[] => {
+ const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset)
if (index < 0) {
throw new Error('You cannot create GDD for a month older than the last three months.')
@@ -137,3 +148,7 @@ export const updateCreations = (creations: Decimal[], contribution: Contribution
creations[index] = creations[index].plus(contribution.amount.toString())
return creations
}
+
+export const isValidDateString = (dateString: string): boolean => {
+ return new Date(dateString).toString() !== 'Invalid Date'
+}
diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json
new file mode 100644
index 000000000..6c270f148
--- /dev/null
+++ b/backend/src/locales/de.json
@@ -0,0 +1,15 @@
+{
+ "emails": {
+ "accountMultiRegistration": {
+ "emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.",
+ "emailReused": "Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
+ "helloName": "Hallo {firstName} {lastName}",
+ "ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:",
+ "onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
+ "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
+ "sincerelyYours": "Mit freundlichen Grüßen,",
+ "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail",
+ "yourGradidoTeam": "dein Gradido-Team"
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json
new file mode 100644
index 000000000..7655aae6a
--- /dev/null
+++ b/backend/src/locales/en.json
@@ -0,0 +1,15 @@
+{
+ "emails": {
+ "accountMultiRegistration": {
+ "emailExists": "However, an account already exists for your email address.",
+ "emailReused": "Your email address has just been used again to register an account with Gradido.",
+ "helloName": "Hello {firstName} {lastName}",
+ "ifYouAreNotTheOne": "If you are not the one who tried to register again, please contact our support:",
+ "onForgottenPasswordClickLink": "Please click on the following link if you have forgotten your password:",
+ "onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
+ "sincerelyYours": "Sincerely yours,",
+ "subject": "Gradido: Try To Register Again With Your Email",
+ "yourGradidoTeam": "your Gradido team"
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts b/backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
deleted file mode 100644
index bb37a196e..000000000
--- a/backend/src/mailer/sendAccountMultiRegistrationEmail.test.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import CONFIG from '@/config'
-import { sendAccountMultiRegistrationEmail } from './sendAccountMultiRegistrationEmail'
-import { sendEMail } from './sendEMail'
-
-jest.mock('./sendEMail', () => {
- return {
- __esModule: true,
- sendEMail: jest.fn(),
- }
-})
-
-describe('sendAccountMultiRegistrationEmail', () => {
- beforeEach(async () => {
- await sendAccountMultiRegistrationEmail({
- firstName: 'Peter',
- lastName: 'Lustig',
- email: 'peter@lustig.de',
- })
- })
-
- it('calls sendEMail', () => {
- expect(sendEMail).toBeCalledWith({
- to: `Peter Lustig `,
- subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail',
- text:
- expect.stringContaining('Hallo Peter Lustig') &&
- expect.stringContaining(CONFIG.EMAIL_LINK_FORGOTPASSWORD) &&
- expect.stringContaining('https://gradido.net/de/contact/'),
- })
- })
-})
diff --git a/backend/src/mailer/sendAccountMultiRegistrationEmail.ts b/backend/src/mailer/sendAccountMultiRegistrationEmail.ts
deleted file mode 100644
index 18928770b..000000000
--- a/backend/src/mailer/sendAccountMultiRegistrationEmail.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { sendEMail } from './sendEMail'
-import { accountMultiRegistration } from './text/accountMultiRegistration'
-import CONFIG from '@/config'
-
-export const sendAccountMultiRegistrationEmail = (data: {
- firstName: string
- lastName: string
- email: string
-}): Promise => {
- return sendEMail({
- to: `${data.firstName} ${data.lastName} <${data.email}>`,
- subject: accountMultiRegistration.de.subject,
- text: accountMultiRegistration.de.text({
- ...data,
- resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
- }),
- })
-}
diff --git a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts
index bed8f6214..9a2ec1aa1 100644
--- a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts
+++ b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts
@@ -26,12 +26,12 @@ describe('sendAddedContributionMessageEmail', () => {
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: `Bibi Bloxberg `,
- subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag',
+ subject: 'Nachricht zu deinem Gemeinwohl-Beitrag',
text:
expect.stringContaining('Hallo Bibi Bloxberg') &&
expect.stringContaining('Peter Lustig') &&
expect.stringContaining(
- 'Du hast soeben zu deinem eingereichten Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" eine Rückfrage von Peter Lustig erhalten.',
+ 'du hast zu deinem Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" eine Nachricht von Peter Lustig erhalten.',
) &&
expect.stringContaining('Was für ein Besen ist es geworden?') &&
expect.stringContaining('http://localhost/overview'),
diff --git a/backend/src/mailer/sendContributionConfirmedEmail.test.ts b/backend/src/mailer/sendContributionConfirmedEmail.test.ts
index 1935144fd..bd89afa69 100644
--- a/backend/src/mailer/sendContributionConfirmedEmail.test.ts
+++ b/backend/src/mailer/sendContributionConfirmedEmail.test.ts
@@ -26,11 +26,11 @@ describe('sendContributionConfirmedEmail', () => {
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: 'Bibi Bloxberg ',
- subject: 'Schöpfung wurde bestätigt',
+ subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt',
text:
expect.stringContaining('Hallo Bibi Bloxberg') &&
expect.stringContaining(
- 'Dein Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben bestätigt.',
+ 'dein Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben von Peter Lustig bestätigt und in deinem Gradido-Konto gutgeschrieben.',
) &&
expect.stringContaining('Betrag: 200,00 GDD') &&
expect.stringContaining('Link zu deinem Konto: http://localhost/overview'),
diff --git a/backend/src/mailer/sendContributionRejectedEmail.test.ts b/backend/src/mailer/sendContributionRejectedEmail.test.ts
index fb044692b..be41ff15f 100644
--- a/backend/src/mailer/sendContributionRejectedEmail.test.ts
+++ b/backend/src/mailer/sendContributionRejectedEmail.test.ts
@@ -26,11 +26,11 @@ describe('sendContributionConfirmedEmail', () => {
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: 'Bibi Bloxberg ',
- subject: 'Schöpfung wurde abgelehnt',
+ subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt',
text:
expect.stringContaining('Hallo Bibi Bloxberg') &&
expect.stringContaining(
- 'Dein Gradido Schöpfungsantrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde soeben von Peter Lustig abgelehnt.',
+ 'dein Gemeinwohl-Beitrag "Vielen herzlichen Dank für den neuen Hexenbesen!" wurde von Peter Lustig abgelehnt.',
) &&
expect.stringContaining('Link zu deinem Konto: http://localhost/overview'),
})
diff --git a/backend/src/mailer/sendEMail.test.ts b/backend/src/mailer/sendEMail.test.ts
index 8e3b0c4a2..e062b71d8 100644
--- a/backend/src/mailer/sendEMail.test.ts
+++ b/backend/src/mailer/sendEMail.test.ts
@@ -38,7 +38,7 @@ describe('sendEMail', () => {
})
})
- it('logs warining', () => {
+ it('logs warning', () => {
expect(logger.info).toBeCalledWith('Emails are disabled via config...')
})
diff --git a/backend/src/mailer/sendTransactionReceivedEmail.test.ts b/backend/src/mailer/sendTransactionReceivedEmail.test.ts
index 9f2ba9938..ca813c033 100644
--- a/backend/src/mailer/sendTransactionReceivedEmail.test.ts
+++ b/backend/src/mailer/sendTransactionReceivedEmail.test.ts
@@ -26,7 +26,7 @@ describe('sendTransactionReceivedEmail', () => {
it('calls sendEMail', () => {
expect(sendEMail).toBeCalledWith({
to: `Peter Lustig `,
- subject: 'Gradido Überweisung',
+ subject: 'Du hast Gradidos erhalten',
text:
expect.stringContaining('Hallo Peter Lustig') &&
expect.stringContaining('42,00 GDD') &&
diff --git a/backend/src/mailer/text/contributionConfirmed.ts b/backend/src/mailer/text/contributionConfirmed.ts
index dc82d7615..106c3a4c5 100644
--- a/backend/src/mailer/text/contributionConfirmed.ts
+++ b/backend/src/mailer/text/contributionConfirmed.ts
@@ -2,7 +2,7 @@ import Decimal from 'decimal.js-light'
export const contributionConfirmed = {
de: {
- subject: 'Schöpfung wurde bestätigt',
+ subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt',
text: (data: {
senderFirstName: string
senderLastName: string
@@ -14,18 +14,17 @@ export const contributionConfirmed = {
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
-Dein eingereichter Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${
- data.senderFirstName
- } ${data.senderLastName} bestätigt.
+dein Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${
+ data.senderLastName
+ } bestätigt und in deinem Gradido-Konto gutgeschrieben.
Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD
+Link zu deinem Konto: ${data.overviewURL}
+
Bitte antworte nicht auf diese E-Mail!
-Mit freundlichen Grüßen,
-dein Gradido-Team
-
-
-Link zu deinem Konto: ${data.overviewURL}`,
+Liebe Grüße
+dein Gradido-Team`,
},
}
diff --git a/backend/src/mailer/text/contributionMessageReceived.ts b/backend/src/mailer/text/contributionMessageReceived.ts
index af1cabb9f..301ebef22 100644
--- a/backend/src/mailer/text/contributionMessageReceived.ts
+++ b/backend/src/mailer/text/contributionMessageReceived.ts
@@ -1,6 +1,6 @@
export const contributionMessageReceived = {
de: {
- subject: 'Rückfrage zu Deinem Gemeinwohl-Beitrag',
+ subject: 'Nachricht zu deinem Gemeinwohl-Beitrag',
text: (data: {
senderFirstName: string
senderLastName: string
@@ -14,15 +14,15 @@ export const contributionMessageReceived = {
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
-du hast soeben zu deinem eingereichten Gemeinwohl-Beitrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten.
+du hast zu deinem Gemeinwohl-Beitrag "${data.contributionMemo}" eine Nachricht von ${data.senderFirstName} ${data.senderLastName} erhalten.
-Bitte beantworte die Rückfrage in deinem Gradido-Konto im Menü "Gemeinschaft" im Tab "Meine Beiträge zum Gemeinwohl"!
+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"!
Link zu deinem Konto: ${data.overviewURL}
Bitte antworte nicht auf diese E-Mail!
-Mit freundlichen Grüßen,
+Liebe Grüße
dein Gradido-Team`,
},
}
diff --git a/backend/src/mailer/text/contributionRejected.ts b/backend/src/mailer/text/contributionRejected.ts
index a101e7a25..ff52c7b5a 100644
--- a/backend/src/mailer/text/contributionRejected.ts
+++ b/backend/src/mailer/text/contributionRejected.ts
@@ -2,7 +2,7 @@ import Decimal from 'decimal.js-light'
export const contributionRejected = {
de: {
- subject: 'Schöpfung wurde abgelehnt',
+ subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt',
text: (data: {
senderFirstName: string
senderLastName: string
@@ -14,14 +14,15 @@ export const contributionRejected = {
}): string =>
`Hallo ${data.recipientFirstName} ${data.recipientLastName},
-Dein eingereichter Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${data.senderLastName} abgelehnt.
-
+dein Gemeinwohl-Beitrag "${data.contributionMemo}" wurde von ${data.senderFirstName} ${data.senderLastName} abgelehnt.
+
+Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü "Gemeinschaft" auf den Tab "Meine Beiträge zum Gemeinwohl"!
+
+Link zu deinem Konto: ${data.overviewURL}
+
Bitte antworte nicht auf diese E-Mail!
-Mit freundlichen Grüßen,
-dein Gradido-Team
-
-
-Link zu deinem Konto: ${data.overviewURL}`,
+Liebe Grüße
+dein Gradido-Team`,
},
}
diff --git a/backend/src/mailer/text/transactionLinkRedeemed.ts b/backend/src/mailer/text/transactionLinkRedeemed.ts
index 4d8e89cae..a63e5d275 100644
--- a/backend/src/mailer/text/transactionLinkRedeemed.ts
+++ b/backend/src/mailer/text/transactionLinkRedeemed.ts
@@ -14,20 +14,20 @@ export const transactionLinkRedeemed = {
memo: string
overviewURL: string
}): string =>
- `Hallo ${data.recipientFirstName} ${data.recipientLastName}
+ `Hallo ${data.recipientFirstName} ${data.recipientLastName},
- ${data.senderFirstName} ${data.senderLastName} (${
+${data.senderFirstName} ${data.senderLastName} (${
data.senderEmail
}) hat soeben deinen Link eingelöst.
-
- Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
- Memo: ${data.memo}
-
- Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
-
- Bitte antworte nicht auf diese E-Mail!
-
- Mit freundlichen Grüßen,
- dein Gradido-Team`,
+
+Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD,
+Memo: ${data.memo}
+
+Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
+
+Bitte antworte nicht auf diese E-Mail!
+
+Liebe Grüße
+dein Gradido-Team`,
},
}
diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts
index ba61ae680..67758c0e1 100644
--- a/backend/src/mailer/text/transactionReceived.ts
+++ b/backend/src/mailer/text/transactionReceived.ts
@@ -2,7 +2,7 @@ import Decimal from 'decimal.js-light'
export const transactionReceived = {
de: {
- subject: 'Gradido Überweisung',
+ subject: 'Du hast Gradidos erhalten',
text: (data: {
senderFirstName: string
senderLastName: string
@@ -13,9 +13,9 @@ export const transactionReceived = {
amount: Decimal
overviewURL: string
}): string =>
- `Hallo ${data.recipientFirstName} ${data.recipientLastName}
+ `Hallo ${data.recipientFirstName} ${data.recipientLastName},
-Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
+du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${
data.senderLastName
} (${data.senderEmail}) erhalten.
@@ -23,7 +23,7 @@ Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL}
Bitte antworte nicht auf diese E-Mail!
-Mit freundlichen Grüßen,
+Liebe Grüße
dein Gradido-Team`,
},
}
diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts
new file mode 100644
index 000000000..971b6a32e
--- /dev/null
+++ b/backend/src/password/EncryptorUtils.ts
@@ -0,0 +1,71 @@
+import CONFIG from '@/config'
+import { backendLogger as logger } from '@/server/logger'
+import { User } from '@entity/User'
+import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
+
+// eslint-disable-next-line @typescript-eslint/no-var-requires
+const sodium = require('sodium-native')
+
+// We will reuse this for changePassword
+export const isValidPassword = (password: string): boolean => {
+ return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/)
+}
+
+export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => {
+ logger.trace('SecretKeyCryptographyCreateKey...')
+ const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
+ const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
+ if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
+ logger.error(
+ `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
+ )
+ throw new Error(
+ `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
+ )
+ }
+
+ const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES)
+ sodium.crypto_hash_sha512_init(state)
+ sodium.crypto_hash_sha512_update(state, Buffer.from(salt))
+ sodium.crypto_hash_sha512_update(state, configLoginAppSecret)
+ const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES)
+ sodium.crypto_hash_sha512_final(state, hash)
+
+ const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES)
+ const opsLimit = 10
+ const memLimit = 33554432
+ const algo = 2
+ sodium.crypto_pwhash(
+ encryptionKey,
+ Buffer.from(password),
+ hash.slice(0, sodium.crypto_pwhash_SALTBYTES),
+ opsLimit,
+ memLimit,
+ algo,
+ )
+
+ const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES)
+ sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey)
+
+ return [encryptionKeyHash, encryptionKey]
+}
+
+export const getUserCryptographicSalt = (dbUser: User): string => {
+ switch (dbUser.passwordEncryptionType) {
+ case PasswordEncryptionType.NO_PASSWORD: {
+ logger.error('Password not set for user ' + dbUser.id)
+ throw new Error('Password not set for user ' + dbUser.id) // user has no password
+ }
+ case PasswordEncryptionType.EMAIL: {
+ return dbUser.emailContact.email
+ break
+ }
+ case PasswordEncryptionType.GRADIDO_ID: {
+ return dbUser.gradidoID
+ break
+ }
+ default:
+ logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
+ throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
+ }
+}
diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts
new file mode 100644
index 000000000..1735106c1
--- /dev/null
+++ b/backend/src/password/PasswordEncryptor.ts
@@ -0,0 +1,14 @@
+import { User } from '@entity/User'
+// import { logger } from '@test/testSetup' getting error "jest is not defined"
+import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils'
+
+export const encryptPassword = (dbUser: User, password: string): bigint => {
+ const salt = getUserCryptographicSalt(dbUser)
+ const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash
+ const passwordHash = keyBuffer[0].readBigUInt64LE()
+ return passwordHash
+}
+
+export const verifyPassword = (dbUser: User, password: string): boolean => {
+ return dbUser.password.toString() === encryptPassword(dbUser, password).toString()
+}
diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts
index c5a55cb84..3675d381d 100644
--- a/backend/src/seeds/index.ts
+++ b/backend/src/seeds/index.ts
@@ -29,6 +29,7 @@ const context = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
forEach: (): void => {},
},
+ clientTimezoneOffset: 0,
}
export const cleanDB = async () => {
diff --git a/backend/src/server/context.ts b/backend/src/server/context.ts
index 5bfc22e72..8ba590dd3 100644
--- a/backend/src/server/context.ts
+++ b/backend/src/server/context.ts
@@ -9,7 +9,7 @@ export interface Context {
setHeaders: { key: string; value: string }[]
role?: Role
user?: dbUser
- clientRequestTime?: string
+ clientTimezoneOffset?: number
// hack to use less DB calls for Balance Resolver
lastTransaction?: dbTransaction
transactionCount?: number
@@ -19,7 +19,7 @@ export interface Context {
const context = (args: ExpressContext): Context => {
const authorization = args.req.headers.authorization
- const clientRequestTime = args.req.headers.clientrequesttime
+ const clientTimezoneOffset = args.req.headers.clienttimezoneoffset
const context: Context = {
token: null,
setHeaders: [],
@@ -27,8 +27,8 @@ const context = (args: ExpressContext): Context => {
if (authorization) {
context.token = authorization.replace(/^Bearer /, '')
}
- if (clientRequestTime && typeof clientRequestTime === 'string') {
- context.clientRequestTime = clientRequestTime
+ if (clientTimezoneOffset && typeof clientTimezoneOffset === 'string') {
+ context.clientTimezoneOffset = Number(clientTimezoneOffset)
}
return context
}
@@ -38,4 +38,14 @@ export const getUser = (context: Context): dbUser => {
throw new Error('No user given in context!')
}
+export const getClientTimezoneOffset = (context: Context): number => {
+ if (
+ (context.clientTimezoneOffset || context.clientTimezoneOffset === 0) &&
+ Math.abs(context.clientTimezoneOffset) <= 27 * 60
+ ) {
+ return context.clientTimezoneOffset
+ }
+ throw new Error('No valid client time zone offset in context!')
+}
+
export default context
diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts
index 8ae4675db..390ff1c6b 100644
--- a/backend/src/server/createServer.ts
+++ b/backend/src/server/createServer.ts
@@ -25,6 +25,9 @@ import { Connection } from '@dbTools/typeorm'
import { apolloLogger } from './logger'
import { Logger } from 'log4js'
+// i18n
+import { i18n } from './localization'
+
// TODO implement
// import queryComplexity, { simpleEstimator, fieldConfigEstimator } from "graphql-query-complexity";
@@ -34,6 +37,7 @@ const createServer = async (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
context: any = serverContext,
logger: Logger = apolloLogger,
+ localization: i18n.I18n = i18n,
): Promise => {
logger.addContext('user', 'unknown')
logger.debug('createServer...')
@@ -63,6 +67,9 @@ const createServer = async (
// bodyparser urlencoded for elopage
app.use(express.urlencoded({ extended: true }))
+ // i18n
+ app.use(localization.init)
+
// Elopage Webhook
app.post('/hook/elopage/' + CONFIG.WEBHOOK_ELOPAGE_SECRET, elopageWebhook)
@@ -80,6 +87,7 @@ const createServer = async (
`running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`,
)
logger.debug('createServer...successful')
+
return { apollo, app, con }
}
diff --git a/backend/src/server/localization.ts b/backend/src/server/localization.ts
new file mode 100644
index 000000000..44fb1b526
--- /dev/null
+++ b/backend/src/server/localization.ts
@@ -0,0 +1,28 @@
+import path from 'path'
+import { backendLogger } from './logger'
+import i18n from 'i18n'
+
+i18n.configure({
+ locales: ['en', 'de'],
+ defaultLocale: 'en',
+ retryInDefaultLocale: false,
+ directory: path.join(__dirname, '..', 'locales'),
+ // autoReload: true, // if this is activated the seeding hangs at the very end
+ updateFiles: false,
+ objectNotation: true,
+ logDebugFn: (msg) => backendLogger.debug(msg),
+ logWarnFn: (msg) => backendLogger.info(msg),
+ logErrorFn: (msg) => backendLogger.error(msg),
+ // this api is needed for email-template pug files
+ api: {
+ __: 't', // now req.__ becomes req.t
+ __n: 'tn', // and req.__n can be called as req.tn
+ },
+ register: global,
+ mustacheConfig: {
+ tags: ['{', '}'],
+ disable: false,
+ },
+})
+
+export { i18n }
diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts
index e885b7043..298348f0f 100644
--- a/backend/src/util/communityUser.ts
+++ b/backend/src/util/communityUser.ts
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
+import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType'
import { SaveOptions, RemoveOptions } from '@dbTools/typeorm'
import { User as dbUser } from '@entity/User'
import { UserContact } from '@entity/UserContact'
@@ -26,6 +27,8 @@ const communityDbUser: dbUser = {
isAdmin: null,
publisherId: 0,
passphrase: '',
+ // default password encryption type
+ passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD,
hasId: function (): boolean {
throw new Error('Function not implemented.')
},
diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts
index 6e1856b63..7ee8e6052 100644
--- a/backend/test/helpers.ts
+++ b/backend/test/helpers.ts
@@ -16,6 +16,7 @@ const context = {
push: headerPushMock,
forEach: jest.fn(),
},
+ clientTimezoneOffset: 0,
}
export const cleanDB = async () => {
@@ -25,8 +26,8 @@ export const cleanDB = async () => {
}
}
-export const testEnvironment = async (logger?: any) => {
- const server = await createServer(context, logger)
+export const testEnvironment = async (logger?: any, localization?: any) => {
+ const server = await createServer(context, logger, localization)
const con = server.con
const testClient = createTestClient(server.apollo)
const mutate = testClient.mutate
@@ -46,3 +47,12 @@ export const resetEntity = async (entity: any) => {
export const resetToken = () => {
context.token = ''
}
+
+// format date string as it comes from the frontend for the contribution date
+export const contributionDateFormatter = (date: Date): string => {
+ return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
+}
+
+export const setClientTimezoneOffset = (offset: number): void => {
+ context.clientTimezoneOffset = offset
+}
diff --git a/backend/test/testSetup.ts b/backend/test/testSetup.ts
index a43335e55..06779674d 100644
--- a/backend/test/testSetup.ts
+++ b/backend/test/testSetup.ts
@@ -1,4 +1,5 @@
import { backendLogger as logger } from '@/server/logger'
+import { i18n } from '@/server/localization'
jest.setTimeout(1000000)
@@ -19,4 +20,18 @@ jest.mock('@/server/logger', () => {
}
})
-export { logger }
+jest.mock('@/server/localization', () => {
+ const originalModule = jest.requireActual('@/server/localization')
+ return {
+ __esModule: true,
+ ...originalModule,
+ i18n: {
+ init: jest.fn(),
+ // configure: jest.fn(),
+ // __: jest.fn(),
+ // setLocale: jest.fn(),
+ },
+ }
+})
+
+export { logger, i18n }
diff --git a/backend/yarn.lock b/backend/yarn.lock
index 57f690d8c..940906cfa 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -192,11 +192,21 @@
dependencies:
"@babel/types" "^7.15.4"
+"@babel/helper-string-parser@^7.18.10":
+ version "7.18.10"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56"
+ integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==
+
"@babel/helper-validator-identifier@^7.14.5", "@babel/helper-validator-identifier@^7.14.9", "@babel/helper-validator-identifier@^7.15.7":
version "7.15.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
+"@babel/helper-validator-identifier@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
+ integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
+
"@babel/helper-validator-option@^7.14.5":
version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz#6e72a1fff18d5dfcb878e1e62f1a021c4b72d5a3"
@@ -225,6 +235,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.15.8.tgz#7bacdcbe71bdc3ff936d510c15dcea7cf0b99016"
integrity sha512-BRYa3wcQnjS/nqI8Ac94pYYpJfojHVvVXJ97+IDCImX4Jc8W8Xv1+47enbruk+q1etOpsQNwnfFcNGw+gtPGxA==
+"@babel/parser@^7.6.0", "@babel/parser@^7.9.6":
+ version "7.18.13"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
+ integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
+
"@babel/plugin-syntax-async-generators@^7.8.4":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -348,6 +363,15 @@
"@babel/helper-validator-identifier" "^7.14.9"
to-fast-properties "^2.0.0"
+"@babel/types@^7.6.1", "@babel/types@^7.9.6":
+ version "7.18.13"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
+ integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==
+ dependencies:
+ "@babel/helper-string-parser" "^7.18.10"
+ "@babel/helper-validator-identifier" "^7.18.6"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -380,6 +404,18 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
+"@hapi/boom@^10.0.0":
+ version "10.0.0"
+ resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.0.tgz#3624831d0a26b3378423b246f50eacea16e04a08"
+ integrity sha512-1YVs9tLHhypBqqinKQRqh7FUERIolarQApO37OWkzD+z6y6USi871Sv746zBPKcIOBuI6g6y4FrwX87mmJ90Gg==
+ dependencies:
+ "@hapi/hoek" "10.x.x"
+
+"@hapi/hoek@10.x.x":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-10.0.1.tgz#ee9da297fabc557e1c040a0f44ee89c266ccc306"
+ integrity sha512-CvlW7jmOhWzuqOqiJQ3rQVLMcREh0eel4IBnxDx2FAcK8g7qoJRQK4L1CPBASoCY6y8e6zuCy3f2g+HWdkzcMw==
+
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
@@ -620,6 +656,72 @@
resolved "https://registry.yarnpkg.com/@josephg/resolvable/-/resolvable-1.0.1.tgz#69bc4db754d79e1a2f17a650d3466e038d94a5eb"
integrity sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==
+"@ladjs/country-language@^0.2.1":
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/@ladjs/country-language/-/country-language-0.2.1.tgz#553f776fa1eb295d0344ed06525a945f94cdafaa"
+ integrity sha512-e3AmT7jUnfNE6e2mx2+cPYiWdFW3McySDGRhQEYE6SksjZTMj0PTp+R9x1xG89tHRTsyMNJFl9J4HtZPWZzi1Q==
+ dependencies:
+ underscore "~1.13.1"
+ underscore.deep "~0.5.1"
+
+"@ladjs/country-language@^1.0.1":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@ladjs/country-language/-/country-language-1.0.2.tgz#438facd9ca5312381dccfd0bbd565103d8471e4c"
+ integrity sha512-hqexlNFTu0NN4TGu17rO/k2l8XRMLgqLwcY9i3Rabls946vnqee8TT2qbhUJ+CiiaE0ShC9yKPdcKJ1veNMmJA==
+
+"@ladjs/i18n@^8.0.1":
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/@ladjs/i18n/-/i18n-8.0.1.tgz#fb6ae221b627e7a4d499f336a09f03ded2ab523b"
+ integrity sha512-7+C6IIf/THrrAhSPPlmd3DIl6Ias7YFr37MeIUxXaipLxNcMnQ7oHIRnznwJ78ZwnhcViTa27rfshbtaH9uD5g==
+ dependencies:
+ "@hapi/boom" "^10.0.0"
+ "@ladjs/country-language" "^1.0.1"
+ boolean "3.2.0"
+ i18n "^0.15.0"
+ i18n-locales "^0.0.5"
+ lodash "^4.17.21"
+ multimatch "5"
+ punycode "^2.1.1"
+ qs "^6.11.0"
+ titleize "2"
+ tlds "^1.231.0"
+
+"@messageformat/core@^3.0.0":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@messageformat/core/-/core-3.0.1.tgz#191e12cf9643704d1fd32e592a3fbdc194dd588e"
+ integrity sha512-yxj2+0e46hcZqJfNf0ZYbC2q6WlcGoh4g11mCyRtTueR0AD8F9z4JMYAS1aOiFG8Vl1LZg/h5hZHKmWTAyZq8g==
+ dependencies:
+ "@messageformat/date-skeleton" "^1.0.0"
+ "@messageformat/number-skeleton" "^1.0.0"
+ "@messageformat/parser" "^5.0.0"
+ "@messageformat/runtime" "^3.0.1"
+ make-plural "^7.0.0"
+ safe-identifier "^0.4.1"
+
+"@messageformat/date-skeleton@^1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.0.1.tgz#980b8babe21a11433b6e1e8f6dc8c4cae4f5f56b"
+ integrity sha512-jPXy8fg+WMPIgmGjxSlnGJn68h/2InfT0TNSkVx0IGXgp4ynnvYkbZ51dGWmGySEK+pBiYUttbQdu5XEqX5CRg==
+
+"@messageformat/number-skeleton@^1.0.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@messageformat/number-skeleton/-/number-skeleton-1.1.0.tgz#eb636738da8abbd35ccbeb84f7d84d63302aeb61"
+ integrity sha512-F0Io+GOSvFFxvp9Ze3L5kAoZ2NnOAT0Mr/jpGNd3fqo8A0t4NxNIAcCdggtl2B/gN2ErkIKSBVPrF7xcW1IGvA==
+
+"@messageformat/parser@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.0.0.tgz#5737e69d7d4a469998b527710f1891174fc1b262"
+ integrity sha512-WiDKhi8F0zQaFU8cXgqq69eYFarCnTVxKcvhAONufKf0oUxbqLMW6JX6rV4Hqh+BEQWGyKKKHY4g1XA6bCLylA==
+ dependencies:
+ moo "^0.5.1"
+
+"@messageformat/runtime@^3.0.1":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/@messageformat/runtime/-/runtime-3.0.1.tgz#94d1f6c43265c28ef7aed98ecfcc0968c6c849ac"
+ integrity sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==
+ dependencies:
+ make-plural "^7.0.0"
+
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
@@ -694,6 +796,14 @@
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
+"@selderee/plugin-htmlparser2@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d"
+ integrity sha512-J3jpy002TyBjd4N/p6s+s90eX42H2eRhK3SbsZuvTDv977/E8p2U3zikdiehyJja66do7FlxLomZLPlvl2/xaA==
+ dependencies:
+ domhandler "^4.2.0"
+ selderee "^0.6.0"
+
"@sindresorhus/is@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -828,6 +938,15 @@
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.10.tgz#61cc8469849e5bcdd0c7044122265c39cec10cf4"
integrity sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==
+"@types/email-templates@^10.0.1":
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/@types/email-templates/-/email-templates-10.0.1.tgz#88f218564a6341092f447fbe110047f6bf3e955a"
+ integrity sha512-IHdgtoOUfMB4t5y5wgm8G0i2/U90GeJPxIEAViMaLlJPCJzaYSlVHXI8bx3qbgbD6gxyOsSRyrFvBSTgNEQc+g==
+ dependencies:
+ "@types/html-to-text" "*"
+ "@types/nodemailer" "*"
+ juice "^8.0.0"
+
"@types/express-serve-static-core@^4.17.18", "@types/express-serve-static-core@^4.17.21":
version "4.17.24"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz#ea41f93bf7e0d59cd5a76665068ed6aab6815c07"
@@ -874,6 +993,11 @@
dependencies:
"@types/node" "*"
+"@types/html-to-text@*":
+ version "8.1.1"
+ resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-8.1.1.tgz#0c5573207c14f618f24da5a2910c510285573094"
+ integrity sha512-QFcqfc7TiVbvIX8Fc2kWUxakruI1Ay6uitaGCYHzI5M0WHQROV5D2XeSaVrK0FmvssivXum4yERVnJsiuH61Ww==
+
"@types/http-assert@*":
version "1.5.3"
resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.3.tgz#ef8e3d1a8d46c387f04ab0f2e8ab8cb0c5078661"
@@ -884,6 +1008,11 @@
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-1.8.1.tgz#e81ad28a60bee0328c6d2384e029aec626f1ae67"
integrity sha512-e+2rjEwK6KDaNOm5Aa9wNGgyS9oSZU/4pfSMMPYNOfjvFI0WVXm29+ITRFr6aKDvvKo7uU1jV68MW4ScsfDi7Q==
+"@types/i18n@^0.13.4":
+ version "0.13.4"
+ resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.13.4.tgz#fe3d27d08337f9d4a972d1f460d1471d6f79e163"
+ integrity sha512-PN4ZsplbpHZ2eaYixFNWkZKN51pcB02K2UKvqHVbrzq2jTO0sChPMuKKYAW1ZbElyHUvPgFeYsz9rqktChGyMw==
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -976,7 +1105,7 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==
-"@types/minimatch@*":
+"@types/minimatch@*", "@types/minimatch@^3.0.3":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
@@ -996,6 +1125,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.21.tgz#6359d8cf73481e312a43886fa50afc70ce5592c6"
integrity sha512-zv8ukKci1mrILYiQOwGSV4FpkZhyxQtuFWGya2GujWg+zVAeRQ4qbaMmWp9vb9889CFA8JECH7lkwCL6Ygg8kA==
+"@types/nodemailer@*":
+ version "6.4.6"
+ resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.6.tgz#ce21b4b474a08f672f182e15982b7945dde1f288"
+ integrity sha512-pD6fL5GQtUKvD2WnPmg5bC2e8kWCAPDwMPmHe/ohQbW+Dy0EcHgZ2oCSuPlWNqk74LS5BVMig1SymQbFMPPK3w==
+ dependencies:
+ "@types/node" "*"
+
"@types/nodemailer@^6.4.4":
version "6.4.4"
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
@@ -1440,6 +1576,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
+array-differ@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b"
+ integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==
+
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
@@ -1470,6 +1611,21 @@ array.prototype.flat@^1.2.4:
define-properties "^1.1.3"
es-abstract "^1.19.0"
+arrify@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
+ integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
+
+asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+ integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==
+
+assert-never@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/assert-never/-/assert-never-1.2.1.tgz#11f0e363bf146205fb08193b5c7b90f4d1cf44fe"
+ integrity sha512-TaTivMB6pYI1kXwrFlEhLeGfOqoDNdTxjCdwRfFFkEA30Eu+k48W34nlok2EYWJfFFzqaEmichdNM7th6M5HNw==
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -1560,6 +1716,13 @@ babel-preset-jest@^27.2.0:
babel-plugin-jest-hoist "^27.2.0"
babel-preset-current-node-syntax "^1.0.0"
+babel-walk@3.0.0-canary-5:
+ version "3.0.0-canary-5"
+ resolved "https://registry.yarnpkg.com/babel-walk/-/babel-walk-3.0.0-canary-5.tgz#f66ecd7298357aee44955f235a6ef54219104b11"
+ integrity sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==
+ dependencies:
+ "@babel/types" "^7.9.6"
+
backo2@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
@@ -1591,6 +1754,11 @@ blake2b@^2.1.1:
blake2b-wasm "^2.4.0"
nanoassert "^2.0.0"
+bluebird@^3.7.2:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
+ integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
+
body-parser@1.19.0, body-parser@^1.18.3:
version "1.19.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
@@ -1612,6 +1780,16 @@ bogon@^1.0.0:
resolved "https://registry.yarnpkg.com/bogon/-/bogon-1.0.0.tgz#66b8cdd269f790e3aa988e157bb34d4ba75ee586"
integrity sha512-mXxtlBtnW8koqFWPUBtKJm97vBSKZRpOvxvMRVun33qQXwMNfQzq9eTcQzKzqEoNUhNqF9t8rDc/wakKCcHMTg==
+boolbase@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+ integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
+
+boolean@3.2.0, boolean@^3.1.4:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b"
+ integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==
+
boxen@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
@@ -1763,6 +1941,37 @@ char-regex@^1.0.2:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
+character-parser@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
+ integrity sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==
+ dependencies:
+ is-regex "^1.0.3"
+
+cheerio-select@^1.5.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.6.0.tgz#489f36604112c722afa147dedd0d4609c09e1696"
+ integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==
+ dependencies:
+ css-select "^4.3.0"
+ css-what "^6.0.1"
+ domelementtype "^2.2.0"
+ domhandler "^4.3.1"
+ domutils "^2.8.0"
+
+cheerio@1.0.0-rc.10:
+ version "1.0.0-rc.10"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e"
+ integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==
+ dependencies:
+ cheerio-select "^1.5.0"
+ dom-serializer "^1.3.2"
+ domhandler "^4.2.0"
+ htmlparser2 "^6.1.0"
+ parse5 "^6.0.1"
+ parse5-htmlparser2-tree-adapter "^6.0.1"
+ tslib "^2.2.0"
+
chokidar@^3.2.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
@@ -1788,6 +1997,11 @@ ci-info@^3.1.1:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6"
integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A==
+ci-info@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128"
+ integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==
+
cjs-module-lexer@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40"
@@ -1864,11 +2078,16 @@ combined-stream@^1.0.8:
dependencies:
delayed-stream "~1.0.0"
-commander@^2.20.3:
+commander@^2.19.0, commander@^2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+commander@^6.1.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
+ 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"
@@ -1900,6 +2119,21 @@ configstore@^5.0.1:
write-file-atomic "^3.0.0"
xdg-basedir "^4.0.0"
+consolidate@^0.16.0:
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16"
+ integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==
+ dependencies:
+ bluebird "^3.7.2"
+
+constantinople@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-4.0.1.tgz#0def113fa0e4dc8de83331a5cf79c8b325213151"
+ integrity sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==
+ dependencies:
+ "@babel/parser" "^7.6.0"
+ "@babel/types" "^7.6.1"
+
content-disposition@0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd"
@@ -1954,6 +2188,17 @@ cross-env@^7.0.3:
dependencies:
cross-spawn "^7.0.1"
+cross-spawn@^6.0.0:
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
+ integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
+ dependencies:
+ nice-try "^1.0.4"
+ path-key "^2.0.1"
+ semver "^5.5.0"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@@ -1963,11 +2208,34 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
+crypto-random-string@3.3.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-3.3.1.tgz#13cee94cac8001e4842501608ef779e0ed08f82d"
+ integrity sha512-5j88ECEn6h17UePrLi6pn1JcLtAiANa3KExyr9y9Z5vo2mv56Gh3I4Aja/B9P9uyMwyxNHAHWv+nE72f30T5Dg==
+ dependencies:
+ type-fest "^0.8.1"
+
crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
+css-select@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
+ integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
+ dependencies:
+ boolbase "^1.0.0"
+ css-what "^6.0.1"
+ domhandler "^4.3.1"
+ domutils "^2.8.0"
+ nth-check "^2.0.1"
+
+css-what@^6.0.1:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
+ integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
+
cssfilter@0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae"
@@ -2025,7 +2293,7 @@ debug@^3.2.6, debug@^3.2.7:
dependencies:
ms "^2.1.1"
-debug@^4.3.4:
+debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -2159,6 +2427,19 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+ integrity sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==
+
+display-notification@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/display-notification/-/display-notification-2.0.0.tgz#49fad2e03289b4f668c296e1855c2cf8ba893d49"
+ integrity sha512-TdmtlAcdqy1NU+j7zlkDdMnCL878zriLaBmoD9quOoq1ySSSGv03l0hXK5CvIFZlIfFI/hizqdQuW+Num7xuhw==
+ dependencies:
+ escape-string-applescript "^1.0.0"
+ run-applescript "^3.0.0"
+
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
@@ -2173,6 +2454,25 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
+doctypes@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
+ integrity sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==
+
+dom-serializer@^1.0.1, dom-serializer@^1.3.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
+ integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==
+ dependencies:
+ domelementtype "^2.0.1"
+ domhandler "^4.2.0"
+ entities "^2.0.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
+ integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
+
domexception@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
@@ -2180,6 +2480,29 @@ domexception@^2.0.1:
dependencies:
webidl-conversions "^5.0.0"
+domhandler@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a"
+ integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==
+ dependencies:
+ domelementtype "^2.0.1"
+
+domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
+ integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
+ dependencies:
+ domelementtype "^2.2.0"
+
+domutils@^2.4.2, domutils@^2.5.2, domutils@^2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+ integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+ dependencies:
+ dom-serializer "^1.0.1"
+ domelementtype "^2.2.0"
+ domhandler "^4.2.0"
+
dot-prop@^5.2.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
@@ -2214,6 +2537,20 @@ electron-to-chromium@^1.3.857:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.864.tgz#6a993bcc196a2b8b3df84d28d5d4dd912393885f"
integrity sha512-v4rbad8GO6/yVI92WOeU9Wgxc4NA0n4f6P1FvZTY+jyY7JHEhw3bduYu60v3Q1h81Cg6eo4ApZrFPuycwd5hGw==
+email-templates@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/email-templates/-/email-templates-10.0.1.tgz#00ed3d394c3b64fa7b8127027e52b01d70c468d4"
+ integrity sha512-LNZKS0WW9XQkjuDZd/4p/1Q/pwqaqXOP3iDxTIVIQY9vuHlIUEcRLFo8/Xh3GtZCBnm181VgvOXIABKTVyTePA==
+ dependencies:
+ "@ladjs/i18n" "^8.0.1"
+ consolidate "^0.16.0"
+ get-paths "^0.0.7"
+ html-to-text "^8.2.0"
+ juice "^8.0.0"
+ lodash "^4.17.21"
+ nodemailer "^6.7.7"
+ preview-email "^3.0.7"
+
emittery@^0.8.1:
version "0.8.1"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.8.1.tgz#bb23cc86d03b30aa75a7f734819dee2e1ba70860"
@@ -2229,6 +2566,11 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+encoding-japanese@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/encoding-japanese/-/encoding-japanese-2.0.0.tgz#fa0226e5469e7b5b69a04fea7d5481bd1fa56936"
+ integrity sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==
+
end-of-stream@^1.1.0:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -2243,6 +2585,11 @@ enquirer@^2.3.5:
dependencies:
ansi-colors "^4.1.1"
+entities@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+ integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
error-ex@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
@@ -2295,11 +2642,21 @@ escape-goat@^2.0.0:
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
+escape-goat@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-3.0.0.tgz#e8b5fb658553fe8a3c4959c316c6ebb8c842b19c"
+ integrity sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==
+
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+escape-string-applescript@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/escape-string-applescript/-/escape-string-applescript-1.0.0.tgz#6f1c2294245d82c63bc03338dc19a94aa8428892"
+ integrity sha512-4/hFwoYaC6TkpDn9A3pTC52zQPArFeXuIfhUtCGYdauTzXVP9H3BDr3oO/QzQehMpLDC7srvYgfwvImPFGfvBA==
+
escape-string-regexp@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
@@ -2542,6 +2899,19 @@ events@^3.3.0:
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+execa@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
+ integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
+ dependencies:
+ cross-spawn "^6.0.0"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@@ -2651,6 +3021,13 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+fast-printf@^1.6.9:
+ version "1.6.9"
+ resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676"
+ integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==
+ dependencies:
+ boolean "^3.1.4"
+
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
@@ -2821,6 +3198,23 @@ get-package-type@^0.1.0:
resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a"
integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==
+get-paths@^0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/get-paths/-/get-paths-0.0.7.tgz#15331086752077cf130166ccd233a1cdbeefcf38"
+ integrity sha512-0wdJt7C1XKQxuCgouqd+ZvLJ56FQixKoki9MrFaO4EriqzXOiH9gbukaDE1ou08S8Ns3/yDzoBAISNPqj6e6tA==
+ dependencies:
+ pify "^4.0.1"
+
+get-port@5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
+ integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+ integrity sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==
+
get-stream@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@@ -3010,6 +3404,11 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
+he@1.2.0, he@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+ 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"
@@ -3036,6 +3435,50 @@ html-escaper@^2.0.0:
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
+html-to-text@8.2.0:
+ version "8.2.0"
+ resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.2.0.tgz#8b35e280ba7fc27710b7aa76d4500aab30731924"
+ integrity sha512-CLXExYn1b++Lgri+ZyVvbUEFwzkLZppjjZOwB7X1qv2jIi8MrMEvxWX5KQ7zATAzTvcqgmtO00M2kCRMtEdOKQ==
+ dependencies:
+ "@selderee/plugin-htmlparser2" "^0.6.0"
+ deepmerge "^4.2.2"
+ he "^1.2.0"
+ htmlparser2 "^6.1.0"
+ minimist "^1.2.6"
+ selderee "^0.6.0"
+
+html-to-text@^8.2.0:
+ version "8.2.1"
+ resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-8.2.1.tgz#4a75b8a1b646149bd71c50527adb568990bf459b"
+ integrity sha512-aN/3JvAk8qFsWVeE9InWAWueLXrbkoVZy0TkzaGhoRBC2gCFEeRLDDJN3/ijIGHohy6H+SZzUQWN/hcYtaPK8w==
+ dependencies:
+ "@selderee/plugin-htmlparser2" "^0.6.0"
+ deepmerge "^4.2.2"
+ he "^1.2.0"
+ htmlparser2 "^6.1.0"
+ minimist "^1.2.6"
+ selderee "^0.6.0"
+
+htmlparser2@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-5.0.1.tgz#7daa6fc3e35d6107ac95a4fc08781f091664f6e7"
+ integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==
+ dependencies:
+ domelementtype "^2.0.1"
+ domhandler "^3.3.0"
+ domutils "^2.4.2"
+ entities "^2.0.0"
+
+htmlparser2@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
+ integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
+ dependencies:
+ domelementtype "^2.0.1"
+ domhandler "^4.0.0"
+ domutils "^2.5.2"
+ entities "^2.0.0"
+
http-cache-semantics@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
@@ -3105,6 +3548,37 @@ hypercore-crypto@^3.3.0:
compact-encoding "^2.5.1"
sodium-universal "^3.0.0"
+i18n-locales@^0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.5.tgz#8f587e598ab982511d7c7db910cb45b8d93cd96a"
+ integrity sha512-Kve1AHy6rqyfJHPy8MIvaKBKhHhHPXV+a/TgMkjp3UBhO3gfWR40ZQn8Xy7LI6g3FhmbvkFtv+GCZy6yvuyeHQ==
+ dependencies:
+ "@ladjs/country-language" "^0.2.1"
+
+i18n@^0.15.0:
+ version "0.15.0"
+ resolved "https://registry.yarnpkg.com/i18n/-/i18n-0.15.0.tgz#dca7a498a4371874db01f6610381a412897306eb"
+ integrity sha512-TUOkuFbl8Y/q7zF0tHdtpk1/TtxH0T+Drp2NFrHhmN1Qs0Sob9/0uVLS2BPVkEXNh2jZrimOiFJk+tkaOumzog==
+ dependencies:
+ "@messageformat/core" "^3.0.0"
+ debug "^4.3.3"
+ fast-printf "^1.6.9"
+ make-plural "^7.0.0"
+ math-interval-parser "^2.0.1"
+ mustache "^4.2.0"
+
+i18n@^0.15.1:
+ version "0.15.1"
+ resolved "https://registry.yarnpkg.com/i18n/-/i18n-0.15.1.tgz#68fb8993c461cc440bc2485d82f72019f2b92de8"
+ integrity sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA==
+ dependencies:
+ "@messageformat/core" "^3.0.0"
+ debug "^4.3.3"
+ fast-printf "^1.6.9"
+ make-plural "^7.0.0"
+ math-interval-parser "^2.0.1"
+ mustache "^4.2.0"
+
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -3112,7 +3586,7 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-iconv-lite@^0.6.2:
+iconv-lite@0.6.3, iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
@@ -3269,6 +3743,19 @@ is-date-object@^1.0.1:
dependencies:
has-tostringtag "^1.0.0"
+is-docker@^2.0.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
+ integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
+
+is-expression@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-4.0.0.tgz#c33155962abf21d0afd2552514d67d2ec16fd2ab"
+ integrity sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==
+ dependencies:
+ acorn "^7.1.1"
+ object-assign "^4.1.1"
+
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -3336,12 +3823,17 @@ is-potential-custom-element-name@^1.0.1:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+is-promise@^2.0.0:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
+ integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
+
is-property@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
integrity sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=
-is-regex@^1.1.4:
+is-regex@^1.0.3, is-regex@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
@@ -3354,6 +3846,11 @@ is-shared-array-buffer@^1.0.1:
resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
+is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+ integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==
+
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@@ -3385,6 +3882,13 @@ is-weakref@^1.0.1:
dependencies:
call-bind "^1.0.0"
+is-wsl@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
+ integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
+ dependencies:
+ is-docker "^2.0.0"
+
is-yarn-global@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
@@ -3852,6 +4356,11 @@ jest@^27.2.4:
import-local "^3.0.2"
jest-cli "^27.2.5"
+js-stringify@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
+ integrity sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==
+
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3967,6 +4476,25 @@ jsonwebtoken@^8.5.1:
ms "^2.1.1"
semver "^5.6.0"
+jstransformer@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
+ integrity sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==
+ dependencies:
+ is-promise "^2.0.0"
+ promise "^7.0.1"
+
+juice@^8.0.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/juice/-/juice-8.1.0.tgz#4ea23362522fe06418229943237ee3751a4fca70"
+ integrity sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==
+ dependencies:
+ cheerio "1.0.0-rc.10"
+ commander "^6.1.0"
+ mensch "^0.3.4"
+ slick "^1.12.2"
+ web-resource-inliner "^6.0.1"
+
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
@@ -4029,11 +4557,38 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
+libbase64@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/libbase64/-/libbase64-1.2.1.tgz#fb93bf4cb6d730f29b92155b6408d1bd2176a8c8"
+ integrity sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==
+
+libmime@5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/libmime/-/libmime-5.1.0.tgz#d9a1c4a85c982fa4e64c2c841f95e3827c3f71d2"
+ integrity sha512-xOqorG21Va+3CjpFOfFTU7SWohHH2uIX9ZY4Byz6J+lvpfvc486tOAT/G9GfbrKtJ9O7NCX9o0aC2lxqbnZ9EA==
+ dependencies:
+ encoding-japanese "2.0.0"
+ iconv-lite "0.6.3"
+ libbase64 "1.2.1"
+ libqp "1.1.0"
+
libphonenumber-js@^1.9.7:
version "1.9.37"
resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.37.tgz#944f59a3618a8f85d9b619767a0b6fb87523f285"
integrity sha512-RnUR4XwiVhMLnT7uFSdnmLeprspquuDtaShAgKTA+g/ms9/S4hQU3/QpFdh3iXPHtxD52QscXLm2W2+QBmvYAg==
+libqp@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8"
+ integrity sha512-4Rgfa0hZpG++t1Vi2IiqXG9Ad1ig4QTmtuZF946QJP4bPqOYC78ixUXgz5TW/wE7lNaNKlplSYTxQ+fR2KZ0EA==
+
+linkify-it@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-4.0.0.tgz#4f2d16879adc637cdfe9056cbc02de30e88ffa32"
+ integrity sha512-QAxkXyzT/TXgwGyY4rTgC95Ex6/lZ5/lYTV9nug6eJt93BCBQGOE47D/g2+/m5J1MrVLr2ot97OXkBZ9bBpR4A==
+ dependencies:
+ uc.micro "^1.0.1"
+
load-json-file@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
@@ -4119,7 +4674,7 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
-lodash@4.x, lodash@^4.7.0:
+lodash@4.x, lodash@^4.17.21, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -4170,6 +4725,30 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+mailparser@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/mailparser/-/mailparser-3.5.0.tgz#5b333b0ef2f063a7db9d24ed95f29efb464cbef3"
+ integrity sha512-mdr2DFgz8LKC0/Q6io6znA0HVnzaPFT0a4TTnLeZ7mWHlkfnm227Wxlq7mHh7AgeP32h7gOUpXvyhSfJJIEeyg==
+ dependencies:
+ encoding-japanese "2.0.0"
+ he "1.2.0"
+ html-to-text "8.2.0"
+ iconv-lite "0.6.3"
+ libmime "5.1.0"
+ linkify-it "4.0.0"
+ mailsplit "5.3.2"
+ nodemailer "6.7.3"
+ tlds "1.231.0"
+
+mailsplit@5.3.2:
+ version "5.3.2"
+ resolved "https://registry.yarnpkg.com/mailsplit/-/mailsplit-5.3.2.tgz#c344c019f631be4f54d5213509637127e3e3dd66"
+ integrity sha512-coES12hhKqagkuBTJoqERX+y9bXNpxbxw3Esd07auuwKYmcagouVlgucyIVRp48fnswMKxcUtLoFn/L1a75ynQ==
+ dependencies:
+ libbase64 "1.2.1"
+ libmime "5.1.0"
+ libqp "1.1.0"
+
make-dir@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@@ -4182,6 +4761,11 @@ make-error@1.x, make-error@^1.1.1:
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
+make-plural@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-7.1.0.tgz#8a0381ff8c9be4f074e0acdc42ec97639c2344f9"
+ integrity sha512-PKkwVlAxYVo98NrbclaQIT4F5Oy+X58PZM5r2IwUSCe3syya6PXkIRCn2XCdz7p58Scgpp50PBeHmepXVDG3hg==
+
makeerror@1.0.x:
version "1.0.11"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@@ -4189,11 +4773,21 @@ makeerror@1.0.x:
dependencies:
tmpl "1.0.x"
+math-interval-parser@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/math-interval-parser/-/math-interval-parser-2.0.1.tgz#e22cd6d15a0a7f4c03aec560db76513da615bed4"
+ integrity sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==
+
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+mensch@^0.3.4:
+ version "0.3.4"
+ resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.4.tgz#770f91b46cb16ea5b204ee735768c3f0c491fecd"
+ integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==
+
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -4239,6 +4833,11 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+mime@^2.4.6:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
+ integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
+
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
@@ -4261,6 +4860,16 @@ minimist@^1.2.0, minimist@^1.2.5:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+minimist@^1.2.6:
+ version "1.2.6"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
+ integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
+
+moo@^0.5.0, moo@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
+ integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==
+
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -4281,6 +4890,22 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+multimatch@5:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6"
+ integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==
+ dependencies:
+ "@types/minimatch" "^3.0.3"
+ array-differ "^3.0.0"
+ array-union "^2.1.0"
+ arrify "^2.0.1"
+ minimatch "^3.0.4"
+
+mustache@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
+
mysql2@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-2.3.0.tgz#600f5cc27e397dfb77b59eac93666434f88e8079"
@@ -4327,11 +4952,33 @@ natural-compare@^1.4.0:
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
+nearley@^2.20.1:
+ version "2.20.1"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.20.1.tgz#246cd33eff0d012faf197ff6774d7ac78acdd474"
+ integrity sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==
+ dependencies:
+ commander "^2.19.0"
+ moo "^0.5.0"
+ railroad-diagrams "^1.0.0"
+ randexp "0.4.6"
+
negotiator@0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
+nice-try@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
+ integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
+
+node-fetch@^2.6.0:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+ integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
+ dependencies:
+ whatwg-url "^5.0.0"
+
node-fetch@^2.6.1:
version "2.6.5"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
@@ -4359,11 +5006,21 @@ node-releases@^1.1.77:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e"
integrity sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==
+nodemailer@6.7.3:
+ version "6.7.3"
+ resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.3.tgz#b73f9a81b9c8fa8acb4ea14b608f5e725ea8e018"
+ integrity sha512-KUdDsspqx89sD4UUyUKzdlUOper3hRkDVkrKh/89G+d9WKsU5ox51NWS4tB1XR5dPUdR4SP0E3molyEfOvSa3g==
+
nodemailer@^6.6.5:
version "6.6.5"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.6.5.tgz#f9f6953cee5cfe82cbea152eeddacf7a0442049a"
integrity sha512-C/v856DBijUzHcHIgGpQoTrfsH3suKIRAGliIzCstatM2cAa+MYX3LuyCrABiO/cdJTxgBBHXxV1ztiqUwst5A==
+nodemailer@^6.7.7:
+ version "6.7.8"
+ resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.8.tgz#9f1af9911314960c0b889079e1754e8d9e3f740a"
+ integrity sha512-2zaTFGqZixVmTxpJRCFC+Vk5eGRd/fYtvIR+dl5u9QXLTQWGIf48x/JXvo58g9sa0bU6To04XUv554Paykum3g==
+
nodemon@^2.0.7:
version "2.0.13"
resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.13.tgz#67d40d3a4d5bd840aa785c56587269cfcf5d24aa"
@@ -4426,6 +5083,13 @@ normalize-url@^4.1.0:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ integrity sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==
+ dependencies:
+ path-key "^2.0.0"
+
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -4433,12 +5097,19 @@ npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
+nth-check@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
+ integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
+ dependencies:
+ boolbase "^1.0.0"
+
nwsapi@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7"
integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==
-object-assign@^4:
+object-assign@^4, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -4507,6 +5178,14 @@ onetime@^5.1.2:
dependencies:
mimic-fn "^2.1.0"
+open@7:
+ version "7.4.2"
+ resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
+ integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==
+ dependencies:
+ is-docker "^2.0.0"
+ is-wsl "^2.1.1"
+
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
@@ -4536,6 +5215,18 @@ p-cancelable@^1.0.0:
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
+p-event@4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/p-event/-/p-event-4.2.0.tgz#af4b049c8acd91ae81083ebd1e6f5cae2044c1b5"
+ integrity sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==
+ dependencies:
+ p-timeout "^3.1.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+ integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==
+
p-limit@^1.1.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
@@ -4564,6 +5255,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
+p-timeout@^3.0.0, p-timeout@^3.1.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe"
+ integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==
+ dependencies:
+ p-finally "^1.0.0"
+
p-try@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
@@ -4574,6 +5272,13 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+p-wait-for@3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f"
+ integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==
+ dependencies:
+ p-timeout "^3.0.0"
+
package-json@^6.3.0:
version "6.5.0"
resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
@@ -4599,11 +5304,26 @@ parse-json@^4.0.0:
error-ex "^1.3.1"
json-parse-better-errors "^1.0.1"
-parse5@6.0.1:
+parse5-htmlparser2-tree-adapter@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
+ integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
+ dependencies:
+ parse5 "^6.0.1"
+
+parse5@6.0.1, parse5@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
+parseley@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.7.0.tgz#9949e3a0ed05c5072adb04f013c2810cf49171a8"
+ integrity sha512-xyOytsdDu077M3/46Am+2cGXEKM9U9QclBDv7fimY7e+BBlxh2JcBp2mgNsmkyA9uvgyTjVzDi7cP1v4hcFxbw==
+ dependencies:
+ moo "^0.5.1"
+ nearley "^2.20.1"
+
parseurl@^1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -4624,6 +5344,11 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+path-key@^2.0.0, path-key@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+ integrity sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==
+
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
@@ -4666,6 +5391,11 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
pirates@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87"
@@ -4731,11 +5461,35 @@ pretty-format@^27.0.0, pretty-format@^27.2.5:
ansi-styles "^5.0.0"
react-is "^17.0.1"
+preview-email@^3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/preview-email/-/preview-email-3.0.7.tgz#b43e997294367f9c7437150bbe61a52e6bc7dca4"
+ integrity sha512-WGko2NiS3d8qoGcC981sXotm7noW/dcv4Cp4wo+X95ek2WwJ4A+aDpw/MzMjMW/johihvmfrfUdUWBbh+HnxCw==
+ dependencies:
+ ci-info "^3.3.2"
+ crypto-random-string "3.3.1"
+ display-notification "2.0.0"
+ get-port "5.1.1"
+ mailparser "^3.5.0"
+ nodemailer "^6.7.7"
+ open "7"
+ p-event "4.2.0"
+ p-wait-for "3.2.0"
+ pug "^3.0.2"
+ uuid "^8.3.2"
+
progress@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+promise@^7.0.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
+ dependencies:
+ asap "~2.0.3"
+
prompts@^2.0.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069"
@@ -4767,6 +5521,109 @@ pstree.remy@^1.1.7:
resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a"
integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==
+pug-attrs@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-3.0.0.tgz#b10451e0348165e31fad1cc23ebddd9dc7347c41"
+ integrity sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==
+ dependencies:
+ constantinople "^4.0.1"
+ js-stringify "^1.0.2"
+ pug-runtime "^3.0.0"
+
+pug-code-gen@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-3.0.2.tgz#ad190f4943133bf186b60b80de483100e132e2ce"
+ integrity sha512-nJMhW16MbiGRiyR4miDTQMRWDgKplnHyeLvioEJYbk1RsPI3FuA3saEP8uwnTb2nTJEKBU90NFVWJBk4OU5qyg==
+ dependencies:
+ constantinople "^4.0.1"
+ doctypes "^1.1.0"
+ js-stringify "^1.0.2"
+ pug-attrs "^3.0.0"
+ pug-error "^2.0.0"
+ pug-runtime "^3.0.0"
+ void-elements "^3.1.0"
+ with "^7.0.0"
+
+pug-error@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-2.0.0.tgz#5c62173cb09c34de2a2ce04f17b8adfec74d8ca5"
+ integrity sha512-sjiUsi9M4RAGHktC1drQfCr5C5eriu24Lfbt4s+7SykztEOwVZtbFk1RRq0tzLxcMxMYTBR+zMQaG07J/btayQ==
+
+pug-filters@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-4.0.0.tgz#d3e49af5ba8472e9b7a66d980e707ce9d2cc9b5e"
+ integrity sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==
+ dependencies:
+ constantinople "^4.0.1"
+ jstransformer "1.0.0"
+ pug-error "^2.0.0"
+ pug-walk "^2.0.0"
+ resolve "^1.15.1"
+
+pug-lexer@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-5.0.1.tgz#ae44628c5bef9b190b665683b288ca9024b8b0d5"
+ integrity sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==
+ dependencies:
+ character-parser "^2.2.0"
+ is-expression "^4.0.0"
+ pug-error "^2.0.0"
+
+pug-linker@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-4.0.0.tgz#12cbc0594fc5a3e06b9fc59e6f93c146962a7708"
+ integrity sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==
+ dependencies:
+ pug-error "^2.0.0"
+ pug-walk "^2.0.0"
+
+pug-load@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-3.0.0.tgz#9fd9cda52202b08adb11d25681fb9f34bd41b662"
+ integrity sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==
+ dependencies:
+ object-assign "^4.1.1"
+ pug-walk "^2.0.0"
+
+pug-parser@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-6.0.0.tgz#a8fdc035863a95b2c1dc5ebf4ecf80b4e76a1260"
+ integrity sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==
+ dependencies:
+ pug-error "^2.0.0"
+ token-stream "1.0.0"
+
+pug-runtime@^3.0.0, pug-runtime@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-3.0.1.tgz#f636976204723f35a8c5f6fad6acda2a191b83d7"
+ integrity sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==
+
+pug-strip-comments@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz#f94b07fd6b495523330f490a7f554b4ff876303e"
+ integrity sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==
+ dependencies:
+ pug-error "^2.0.0"
+
+pug-walk@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-2.0.0.tgz#417aabc29232bb4499b5b5069a2b2d2a24d5f5fe"
+ integrity sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==
+
+pug@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/pug/-/pug-3.0.2.tgz#f35c7107343454e43bc27ae0ff76c731b78ea535"
+ integrity sha512-bp0I/hiK1D1vChHh6EfDxtndHji55XP/ZJKwsRqrz6lRia6ZC2OZbdAymlxdVFwd1L70ebrVJw4/eZ79skrIaw==
+ dependencies:
+ pug-code-gen "^3.0.2"
+ pug-filters "^4.0.0"
+ pug-lexer "^5.0.1"
+ pug-linker "^4.0.0"
+ pug-load "^3.0.0"
+ pug-parser "^6.0.0"
+ pug-runtime "^3.0.1"
+ pug-strip-comments "^2.0.0"
+
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
@@ -4792,6 +5649,13 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+qs@^6.11.0:
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
+ integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+ dependencies:
+ side-channel "^1.0.4"
+
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
@@ -4802,6 +5666,19 @@ queue-tick@^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:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+ integrity sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==
+
+randexp@0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ integrity sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
random-bigint@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/random-bigint/-/random-bigint-0.0.1.tgz#684de0a93784ab7448a441393916f0e632c95df9"
@@ -4927,7 +5804,7 @@ resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0:
is-core-module "^2.2.0"
path-parse "^1.0.6"
-resolve@^1.17.0:
+resolve@^1.15.1, resolve@^1.17.0:
version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
@@ -4943,6 +5820,11 @@ responselike@^1.0.2:
dependencies:
lowercase-keys "^1.0.0"
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+ integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
+
retry@0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
@@ -4965,6 +5847,13 @@ rimraf@^3.0.0, rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
+run-applescript@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-3.2.0.tgz#73fb34ce85d3de8076d511ea767c30d4fdfc918b"
+ integrity sha512-Ep0RsvAjnRcBX1p5vogbaBdAGu/8j/ewpvGqnQYunnLd9SM0vWcPJewPKNnWFggf0hF0pwIgwV5XK7qQ7UZ8Qg==
+ dependencies:
+ execa "^0.10.0"
+
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
@@ -4982,6 +5871,11 @@ safe-buffer@^5.0.1:
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+safe-identifier@^0.4.1:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb"
+ integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==
+
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
@@ -4999,6 +5893,13 @@ saxes@^5.0.1:
dependencies:
xmlchars "^2.2.0"
+selderee@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.6.0.tgz#f3bee66cfebcb6f33df98e4a1df77388b42a96f7"
+ integrity sha512-ibqWGV5aChDvfVdqNYuaJP/HnVBhlRGSRrlbttmlMpHcLuTqqbMH36QkSs9GEgj5M88JDYLI8eyP94JaQ8xRlg==
+ dependencies:
+ parseley "^0.7.0"
+
semver-diff@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
@@ -5006,7 +5907,7 @@ semver-diff@^3.1.1:
dependencies:
semver "^6.3.0"
-"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.1:
+"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
@@ -5107,6 +6008,13 @@ sha512-wasm@^2.3.1:
b4a "^1.0.1"
nanoassert "^2.0.0"
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ integrity sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==
+ dependencies:
+ shebang-regex "^1.0.0"
+
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@@ -5114,6 +6022,11 @@ shebang-command@^2.0.0:
dependencies:
shebang-regex "^3.0.0"
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+ integrity sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==
+
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
@@ -5128,6 +6041,11 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
+signal-exit@^3.0.0:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
signal-exit@^3.0.2, signal-exit@^3.0.3:
version "3.0.5"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
@@ -5159,6 +6077,11 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0"
+slick@^1.12.2:
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7"
+ integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==
+
sodium-javascript@~0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/sodium-javascript/-/sodium-javascript-0.8.0.tgz#0a94d7bb58ab17be82255f3949259af59778fdbc"
@@ -5346,6 +6269,11 @@ strip-bom@^4.0.0:
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878"
integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+ integrity sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==
+
strip-final-newline@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
@@ -5465,6 +6393,16 @@ timeout-refresh@^2.0.0:
resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-2.0.1.tgz#f8ec7cf1f9d93b2635b7d4388cb820c5f6c16f98"
integrity sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==
+titleize@2:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f"
+ integrity sha512-m+apkYlfiQTKLW+sI4vqUkwMEzfgEUEYSqljx1voUE3Wz/z1ZsxyzSxvH2X8uKVrOp7QkByWt0rA6+gvhCKy6g==
+
+tlds@1.231.0, tlds@^1.231.0:
+ version "1.231.0"
+ resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.231.0.tgz#93880175cd0a06fdf7b5b5b9bcadff9d94813e39"
+ integrity sha512-L7UQwueHSkGxZHQBXHVmXW64oi+uqNtzFt2x6Ssk7NVnpIbw16CRs4eb/jmKOZ9t2JnqZ/b3Cfvo97lnXqKrhw==
+
tmpl@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
@@ -5492,6 +6430,11 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+token-stream@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-1.0.0.tgz#cc200eab2613f4166d27ff9afc7ca56d49df6eb4"
+ integrity sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==
+
touch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b"
@@ -5589,6 +6532,11 @@ tslib@^2.0.1, tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
+tslib@^2.2.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
+ integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
+
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
@@ -5625,6 +6573,11 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
+type-fest@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
+ integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
+
type-graphql@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/type-graphql/-/type-graphql-1.1.1.tgz#dc0710d961713b92d3fee927981fa43bf71667a4"
@@ -5659,6 +6612,11 @@ typescript@^4.3.4:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
+uc.micro@^1.0.1:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
+ 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"
@@ -5686,6 +6644,16 @@ undefsafe@^2.0.3:
dependencies:
debug "^2.2.0"
+underscore.deep@~0.5.1:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/underscore.deep/-/underscore.deep-0.5.3.tgz#210969d58025339cecabd2a2ad8c3e8925e5c095"
+ integrity sha512-4OuSOlFNkiVFVc3khkeG112Pdu1gbitMj7t9B9ENb61uFmN70Jq7Iluhi3oflcSgexkKfDdJ5XAJET2gEq6ikA==
+
+underscore@~1.13.1:
+ version "1.13.4"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.4.tgz#7886b46bbdf07f768e0052f1828e1dcab40c0dee"
+ integrity sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==
+
unique-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
@@ -5782,6 +6750,11 @@ v8-to-istanbul@^8.1.0:
convert-source-map "^1.6.0"
source-map "^0.7.3"
+valid-data-url@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-3.0.1.tgz#826c1744e71b5632e847dd15dbd45b9fb38aa34f"
+ integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==
+
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@@ -5800,6 +6773,11 @@ vary@^1, vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+void-elements@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
+ integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
+
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@@ -5821,6 +6799,18 @@ walker@^1.0.7:
dependencies:
makeerror "1.0.x"
+web-resource-inliner@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz#df0822f0a12028805fe80719ed52ab6526886e02"
+ integrity sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==
+ dependencies:
+ ansi-colors "^4.1.1"
+ escape-goat "^3.0.0"
+ htmlparser2 "^5.0.0"
+ mime "^2.4.6"
+ node-fetch "^2.6.0"
+ valid-data-url "^3.0.0"
+
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
@@ -5876,6 +6866,13 @@ which-boxed-primitive@^1.0.2:
is-string "^1.0.5"
is-symbol "^1.0.3"
+which@^1.2.9:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
+ integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
+ dependencies:
+ isexe "^2.0.0"
+
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
@@ -5890,6 +6887,16 @@ widest-line@^3.1.0:
dependencies:
string-width "^4.0.0"
+with@^7.0.0:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/with/-/with-7.0.2.tgz#ccee3ad542d25538a7a7a80aad212b9828495bac"
+ integrity sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==
+ dependencies:
+ "@babel/parser" "^7.9.6"
+ "@babel/types" "^7.9.6"
+ assert-never "^1.2.1"
+ babel-walk "3.0.0-canary-5"
+
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts
new file mode 100644
index 000000000..2a3332925
--- /dev/null
+++ b/database/entity/0053-change_password_encryption/User.ts
@@ -0,0 +1,127 @@
+import {
+ BaseEntity,
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ DeleteDateColumn,
+ OneToMany,
+ JoinColumn,
+ OneToOne,
+} from 'typeorm'
+import { Contribution } from '../Contribution'
+import { ContributionMessage } from '../ContributionMessage'
+import { UserContact } from '../UserContact'
+
+@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
+export class User extends BaseEntity {
+ @PrimaryGeneratedColumn('increment', { unsigned: true })
+ id: number
+
+ @Column({
+ name: 'gradido_id',
+ length: 36,
+ nullable: false,
+ collation: 'utf8mb4_unicode_ci',
+ })
+ gradidoID: string
+
+ @Column({
+ name: 'alias',
+ length: 20,
+ nullable: true,
+ default: null,
+ collation: 'utf8mb4_unicode_ci',
+ })
+ alias: string
+
+ @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true })
+ pubKey: Buffer
+
+ @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true })
+ privKey: Buffer
+
+ @Column({
+ type: 'text',
+ name: 'passphrase',
+ collation: 'utf8mb4_unicode_ci',
+ nullable: true,
+ default: null,
+ })
+ passphrase: string
+
+ @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user)
+ @JoinColumn({ name: 'email_id' })
+ emailContact: UserContact
+
+ @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null })
+ emailId: number | null
+
+ @Column({
+ name: 'first_name',
+ length: 255,
+ nullable: true,
+ default: null,
+ collation: 'utf8mb4_unicode_ci',
+ })
+ firstName: string
+
+ @Column({
+ name: 'last_name',
+ length: 255,
+ nullable: true,
+ default: null,
+ collation: 'utf8mb4_unicode_ci',
+ })
+ lastName: string
+
+ @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
+ createdAt: Date
+
+ @DeleteDateColumn({ name: 'deleted_at', nullable: true })
+ deletedAt: Date | null
+
+ @Column({ type: 'bigint', default: 0, unsigned: true })
+ password: BigInt
+
+ @Column({
+ name: 'password_encryption_type',
+ type: 'int',
+ unsigned: true,
+ nullable: false,
+ default: 0,
+ })
+ passwordEncryptionType: number
+
+ @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false })
+ language: string
+
+ @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null })
+ isAdmin: Date | null
+
+ @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null })
+ referrerId?: number | null
+
+ @Column({
+ name: 'contribution_link_id',
+ type: 'int',
+ unsigned: true,
+ nullable: true,
+ default: null,
+ })
+ contributionLinkId?: number | null
+
+ @Column({ name: 'publisher_id', default: 0 })
+ publisherId: number
+
+ @OneToMany(() => Contribution, (contribution) => contribution.user)
+ @JoinColumn({ name: 'user_id' })
+ contributions?: Contribution[]
+
+ @OneToMany(() => ContributionMessage, (message) => message.user)
+ @JoinColumn({ name: 'user_id' })
+ messages?: ContributionMessage[]
+
+ @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user)
+ @JoinColumn({ name: 'user_id' })
+ userContacts?: UserContact[]
+}
diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts
new file mode 100644
index 000000000..97b12d4cd
--- /dev/null
+++ b/database/entity/0053-change_password_encryption/UserContact.ts
@@ -0,0 +1,60 @@
+import {
+ BaseEntity,
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ DeleteDateColumn,
+ OneToOne,
+} from 'typeorm'
+import { User } from './User'
+
+@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' })
+export class UserContact extends BaseEntity {
+ @PrimaryGeneratedColumn('increment', { unsigned: true })
+ id: number
+
+ @Column({
+ name: 'type',
+ length: 100,
+ nullable: true,
+ default: null,
+ collation: 'utf8mb4_unicode_ci',
+ })
+ type: string
+
+ @OneToOne(() => User, (user) => user.emailContact)
+ user: User
+
+ @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false })
+ userId: number
+
+ @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' })
+ email: string
+
+ @Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true })
+ emailVerificationCode: BigInt
+
+ @Column({ name: 'email_opt_in_type_id' })
+ emailOptInTypeId: number
+
+ @Column({ name: 'email_resend_count' })
+ emailResendCount: number
+
+ // @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true })
+ // emailHash: Buffer
+
+ @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false })
+ emailChecked: boolean
+
+ @Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' })
+ phone: string
+
+ @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false })
+ createdAt: Date
+
+ @Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' })
+ updatedAt: Date | null
+
+ @DeleteDateColumn({ name: 'deleted_at', nullable: true })
+ deletedAt: Date | null
+}
diff --git a/database/entity/User.ts b/database/entity/User.ts
index d073f428a..b3c00a9b4 100644
--- a/database/entity/User.ts
+++ b/database/entity/User.ts
@@ -1 +1 @@
-export { User } from './0049-add_user_contacts_table/User'
+export { User } from './0053-change_password_encryption/User'
diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts
index a368bb7ca..dd74e65c4 100644
--- a/database/entity/UserContact.ts
+++ b/database/entity/UserContact.ts
@@ -1 +1 @@
-export { UserContact } from './0049-add_user_contacts_table/UserContact'
+export { UserContact } from './0053-change_password_encryption/UserContact'
diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts
new file mode 100644
index 000000000..635109430
--- /dev/null
+++ b/database/migrations/0053-change_password_encryption.ts
@@ -0,0 +1,24 @@
+/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS
+ *
+ * This migration adds and renames columns in the table `users`
+ */
+
+/* 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>) {
+ await queryFn('ALTER TABLE users RENAME COLUMN created TO created_at;')
+ await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;')
+ // alter table emp rename column emp_name to name
+ await queryFn(
+ 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;',
+ )
+ await queryFn(`UPDATE users SET password_encryption_type = 1 WHERE id IN
+ (SELECT user_id FROM user_contacts WHERE email_checked = 1)`)
+}
+
+export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {
+ await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;')
+ await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;')
+ await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;')
+}
diff --git a/database/package.json b/database/package.json
index 096c7a9bd..6216a25fb 100644
--- a/database/package.json
+++ b/database/package.json
@@ -1,6 +1,6 @@
{
"name": "gradido-database",
- "version": "1.13.3",
+ "version": "1.14.1",
"description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database",
diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md
new file mode 100644
index 000000000..fa8573448
--- /dev/null
+++ b/docu/RoadMap_2022-2023.md
@@ -0,0 +1,167 @@
+# Roadmap 2022 / 2023
+
+## unsortierte Sammlung von Themen
+
+1. backend access layer
+
+ - Refactoring der Resolver-Klassen
+ - Daten-Zugriffschicht zur Kapselung der DB-Schicht
+ - Transfer-Datenmodel zum Austausch von Daten zwischen den Schichten
+ - technisches Transaktion-Handling und Lösung von Deadlocks
+ - Konzept in Arbeit
+2. capturing alias
+
+ - Konzept fertig
+ - Änderungen in Register- und Login-Prozess
+3. Passwort-Verschlüsselung: Refactoring
+
+ - Konzept aufteilen in Ausbaustufen
+ - Altlasten entsorgen
+ - Versionierung/Typisierung der verwendeten Verschlüsselungslogik notwendig
+ - DB-Migration auf encryptionType=EMAIL
+4. Passwort-Verschlüsselung: Login mit impliziter Neuverschlüsselung
+
+ * Logik der Passwortverschlüsselung auf GradidoID einführen
+ * bei Login mit encryptionType=Email oder OneTime triggern einer Neuverschlüsselung per GradidoID
+ * Unabhängigkeit von Email erzeugen
+ * Änderung der User-Email ermöglichen
+5. Contribution-Categories
+
+ - Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet?
+ - Regeln auf Categories ermöglichen
+ - Konzept in Arbeit
+6. Statistics / Analysen
+7. Contribution-Link editieren
+8. User-Tagging
+
+ - Eine UserTag dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User
+ - Motivation des User-Taggings: bilden kleinerer lokaler User-Gruppen und jeder kennt jeden
+ - Einführung einer UserTaggings-Tabelle und eine User-UserTaggings-Zuordnungs-Tabelle
+ - Ein Moderator kann im AdminInterface die Liste der UserTags pflegen
+
+ - neues TAG anlegen
+ - vorhandenes TAG umbenennen
+ - ein TAG löschen, sofern kein User mehr diesem TAG zugeordnet ist
+ - Will ein User ein TAG zugeordnet werden, so kann dies nur ein Moderator im AdminInterface tun
+ - Ein Moderator kann im AdminInterface
+
+ - ein TAG einem User zuordnen
+ - ein TAG von einem User entfernen
+ - wichtige UseCases:
+
+ - Zuordnung eines Users zu einem TAG durch einen Moderator
+ - TAG spezifische Schöpfung
+ - User muss für seinen Beitrag ein TAG auswählen können, dem er zuvor zugeordnet wurde
+ - TAG-Moderator kann den Beitrag bestätigen, weil er den User mit dem TAG (persönlich) kennt
+9. User-Beziehungen und Favoritenverwaltung
+
+ - User-User-Zuordnung
+ - aus Tx-Liste die aktuellen Favoriten ermitteln
+ - Verwaltung von Zuordnungen
+ - Auswahl
+ - Berechtigungen
+ - Gruppierung
+ - Community-übergreifend
+ - User-Beziehungen
+10. technische Ablösung der Email und Ersatz durch GradidoID
+
+ * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird
+ * Email soll aber im Aussen für User optional noch verwendbar bleiben
+ * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird
+11. Zeitzone
+
+ - User sieht immer seine Locale-Zeit und Monate
+ - Admin sieht immer UTC-Zeit und Monate
+ - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate)
+ - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)**
+ - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja
+ - Beispiel: User in Tokyo Locale mit Offest +09:00
+
+ - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung
+ - die Contribution wird gespeichert mit
+
+ - creationDate=31.10.2022 22:00:00 UTC
+ - contributionDate=01.11.2022 07:00:00
+ - (neu) clientRequestTime=01.11.2022 07:00:00+09:00
+ - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit
+
+ - creationDate=31.10.2022 22:00:00 UTC
+ - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h**
+
+ - Prüfung auf Sommerzeiten und exotische Länder beachten
+ -
+ - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern
+ - Beispiel für täglichen Contribution-Link während des Monats:
+
+ - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022
+ - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!!
+ - Beispiel für täglichen Contribution-Link am Monatswechsel:
+
+ - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022
+ - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!!
+12. Layout
+13. Lastschriften-Link
+14. Registrierung mit Redeem-Link:
+
+ * bei inaktivem Konto, sprich bisher noch keine Email-Bestätigung, keine Buchung möglich
+ * somit speichern des Links zusammen mit OptIn-Code
+ * damit kann in einem Resend der ConfirmationEmail der Link auch korrekt wieder mitgeliefert werden
+15. Manuelle User-Registrierung für Admin
+
+ - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein
+16. Dezentralisierung / Federation
+
+ - Hyperswarm
+
+ - funktioniert schon im Prototyp
+ - alle Instanzen finden sich gegenseitig
+ - ToDo:
+ - Infos aus HyperSwarm in der Community speichern
+ - Prüfung ob neue mir noch unbekannte Community hinzugekommen ist?
+ - Triggern der Authentifizierungs- und Autorisierungs-Handshake für neue Community
+ - Authentifizierungs- und Autorisierungs-Handshake
+ - Inter-Community-Communication
+ - **ToDos**:
+
+ - DB-Migration für Community-Tabelle, User-Community-Zuordnungen, UserRights-Tabelle
+ - Berechtigungen für Communities
+ - Register- und Login-Prozess für Community-Anmeldung anpassen
+
+ - Auswahl-Box einer Community
+ - createUser mit Zuordnung zur ausgewählten Community
+ - Schöpfungsprozess auf angemeldete Community anpassen
+
+ - "Beitrag einreichen"-Dialog auf angemeldete Community anpassen
+ - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete Community anpassen
+ - "Gemeinschaft"-Dialog auf angemeldete Community anpassen
+ - "Mein Profil"-Dialog auf Communities anpassen
+
+ - Umzug-Service in andere Community
+ - Löschen der Mitgliedschaft zu angemeldeter Community (Deaktivierung der Zuordnung "User-Community")
+ - "Senden"-Dialog mit Community-Auswahl
+ - "Transaktion"-Dialog mit Filter auf angemeldeter Community
+ - AdminInterface auf angemeldete Community anpassen
+
+ - "Übersicht"-Dialog mit Filter auf angemeldete Community
+ - "Nutzersuche"-Dialog mit Filter auf angemeldete Community
+ - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete Comunity
+ - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete Community anpassen
+
+## Priorisierung
+
+1. Contribution-Link editieren (vlt schon im vorherigen Bugfix-Release Ende Okt. 2022 fertig)
+2. Passwort-Verschlüsselung: Refactoring **Konzeption fertig!!**!
+3. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption ongoing!!**!
+4. Passwort-Verschlüsselung: implizite Login-Neuverschlüsselung **Konzeption fertig!!**!
+5. Layout
+6. Zeitzone
+7. Dezentralisierung / Federation
+8. capturing alias **Konzeption fertig!!**!
+9. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich
+10. Subgruppierung / User-Tagging (einfacher Ansatz)
+11. backend access layer
+12. technische Ablösung der Email und Ersatz durch GradidoID
+13. User-Beziehungen und Favoritenverwaltung
+14. Lastschriften-Link
+15. Contribution-Categories
+16. Statistics / Analysen
diff --git a/docu/graphics/RoadMap2022-2023.drawio b/docu/graphics/RoadMap2022-2023.drawio
new file mode 100644
index 000000000..58b8dec94
--- /dev/null
+++ b/docu/graphics/RoadMap2022-2023.drawio
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docu/graphics/RoadMap2022-2023.png b/docu/graphics/RoadMap2022-2023.png
new file mode 100644
index 000000000..3ce8511a3
Binary files /dev/null and b/docu/graphics/RoadMap2022-2023.png differ
diff --git a/frontend/package.json b/frontend/package.json
index 4e983d716..cfc12630e 100755
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "bootstrap-vue-gradido-wallet",
- "version": "1.13.3",
+ "version": "1.14.1",
"private": true,
"scripts": {
"start": "node run/server.js",
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js
index aba5abc34..42deac9cb 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.spec.js
@@ -67,9 +67,9 @@ describe('ContributionMessagesFormular', () => {
await wrapper.find('form').trigger('submit')
})
- it('emitted "get-list-contribution-messages" with data', async () => {
+ it('emitted "get-list-contribution-messages" with false', async () => {
expect(wrapper.emitted('get-list-contribution-messages')).toEqual(
- expect.arrayContaining([expect.arrayContaining([42])]),
+ expect.arrayContaining([expect.arrayContaining([false])]),
)
})
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue
index 1a5928cc3..c601de4f5 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue
@@ -51,7 +51,7 @@ export default {
},
})
.then((result) => {
- this.$emit('get-list-contribution-messages', this.contributionId)
+ this.$emit('get-list-contribution-messages', false)
this.$emit('update-state', this.contributionId)
this.form.text = ''
this.toastSuccess(this.$t('message.reply'))
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js b/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js
index 7798532b7..c5c26a2c0 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesList.spec.js
@@ -40,16 +40,6 @@ describe('ContributionMessagesList', () => {
expect(wrapper.findComponent({ name: 'ContributionMessagesFormular' }).exists()).toBe(true)
})
- describe('get List Contribution Messages', () => {
- beforeEach(() => {
- wrapper.vm.getListContributionMessages()
- })
-
- it('emits getListContributionMessages', async () => {
- expect(wrapper.vm.$emit('get-list-contribution-messages')).toBeTruthy()
- })
- })
-
describe('update State', () => {
beforeEach(() => {
wrapper.vm.updateState()
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesList.vue b/frontend/src/components/ContributionMessages/ContributionMessagesList.vue
index 4b7045a40..e9262c073 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesList.vue
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesList.vue
@@ -9,7 +9,7 @@
@@ -50,9 +50,6 @@ export default {
},
},
methods: {
- getListContributionMessages() {
- this.$emit('get-list-contribution-messages', this.contributionId)
- },
updateState(id) {
this.$emit('update-state', id)
},
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js
index 2dc9fb3ce..1a918747f 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.spec.js
@@ -5,9 +5,11 @@ import ContributionMessagesListItem from './ContributionMessagesListItem.vue'
const localVue = global.localVue
let wrapper
+const dateMock = jest.fn((d) => d)
+
const mocks = {
$t: jest.fn((t) => t),
- $d: jest.fn((d) => d),
+ $d: dateMock,
$store: {
state: {
firstName: 'Peter',
@@ -239,4 +241,63 @@ and here is the link to the repository: https://github.com/gradido/gradido`)
})
})
})
+
+ describe('contribution message type HISTORY', () => {
+ const propsData = {
+ message: {
+ id: 111,
+ message: `Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time)
+---
+This message also contains a link: https://gradido.net/de/
+---
+350.00`,
+ createdAt: '2022-08-29T12:23:27.000Z',
+ updatedAt: null,
+ type: 'HISTORY',
+ userFirstName: 'Peter',
+ userLastName: 'Lustig',
+ userId: 107,
+ __typename: 'ContributionMessage',
+ },
+ }
+
+ const itemWrapper = () => {
+ return mount(ContributionMessagesListItem, {
+ localVue,
+ mocks,
+ propsData,
+ })
+ }
+
+ let messageField
+
+ describe('render HISTORY message', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ wrapper = itemWrapper()
+ messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)')
+ })
+
+ it('renders the date', () => {
+ expect(dateMock).toBeCalledWith(
+ new Date('Sun Nov 13 2022 13:05:48 GMT+0100 (Central European Standard Time'),
+ 'short',
+ )
+ })
+
+ it('renders the amount', () => {
+ expect(messageField.text()).toContain('350.00 GDD')
+ })
+
+ it('contains the link as text', () => {
+ expect(messageField.text()).toContain(
+ 'This message also contains a link: https://gradido.net/de/',
+ )
+ })
+
+ it('contains a link to the given address', () => {
+ expect(messageField.find('a').attributes('href')).toBe('https://gradido.net/de/')
+ })
+ })
+ })
})
diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue
index 9c7a3a0f2..5862f97f5 100644
--- a/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue
+++ b/frontend/src/components/ContributionMessages/ContributionMessagesListItem.vue
@@ -4,25 +4,25 @@
{{ message.userFirstName }} {{ message.userLastName }}
{{ $d(new Date(message.createdAt), 'short') }}
-
+
{{ message.userFirstName }} {{ message.userLastName }}
{{ $d(new Date(message.createdAt), 'short') }}
{{ $t('community.moderator') }}
-
+