From b7a35d167bc463b920cde6dfa2d58972fed5ea36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 13 Sep 2022 22:33:16 +0200 Subject: [PATCH 01/66] first draft --- .../Zeitzonen_Behandlung.md | 95 ++++++++ .../graphics/ZeitzonenKonstellationen.drawio | 217 ++++++++++++++++++ .../image/ZeitzonenKonstellationen.png | Bin 0 -> 47231 bytes 3 files changed, 312 insertions(+) create mode 100644 docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md create mode 100644 docu/Concepts/BusinessRequirements/graphics/ZeitzonenKonstellationen.drawio create mode 100644 docu/Concepts/BusinessRequirements/image/ZeitzonenKonstellationen.png diff --git a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md new file mode 100644 index 000000000..cc681c33a --- /dev/null +++ b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md @@ -0,0 +1,95 @@ +# Zeitzonen + +Die Gradido-Anwendung läuft im Backend in der Zeitzone UTC und im Frontend in der jeweiligen lokalen Zeitzone, in der der User sich anmeldet. Dadurch kann es zu zeitlichen Diskrepanzen kommen, die innerhalb der Anwendungslogik aufgelöst bzw. entsprechend behandelt werden müssen. In den folgenden Kapiteln werden die verschiedenen zeitlichen Konstellationen dargestellt und für die verschiedenen fachlichen Prozesse die daraus resultierenden Problemlösungen beschrieben. + +![img](./image/ZeitzonenKonstellationen.png) + +## Beispiel 1 + +Ein User meldet sich in einer Zeitzone t0 - 4 an. Das bedeutet der User liegt 4 Stunden gegenüber der Backend-Zeit zurück. + +Konkret hat der User die Zeit 31.08.2022 21:00:00 auf dem Server ist aber die Zeit bei 01.09.2022 01:00:00 + +Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl: + + Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD + +**aber das Backend liefert nur die Beträge, die eigentlich so korrekt wären!!!!!** + + **Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD** + +Er möchte für den Juni 2022 eine Contribution mit 500 GDD erfassen. **Wird ihm der Juni noch als Schöpfungsmonat angezeigt?** + +Falls ja, dann wählt er dabei im FE im Kalender den 30.06.2022. Dann liefert das FE folgende Contribution-Daten an das Backend: + +* Gültigkeitsdatum: 30.06.2022 00:00:00 +* Memo: text +* Betrag: 500 GDD +* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert? Das fehlt: entweder über eine Zeit vom FE zum BE und ermitteln Offset im BE** + +Im Backend wird dieses dann interpretiert und verarbeitet mit: + +* **Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] oder [7, 8, 9] da auf dem Server ja schon der 01.09.2022 ist?** +* Gültigkeitsdatum: **30.06.2022 00:00:00 oder 01.07.2022 04:00:00 ?** +* Memo: text +* Betrag 500 GDD +* created_at: 01.07.2022 04:00:00 + +**Frage: wird die Contribution dem Juni (6) oder dem Juli (7) zugeordnet?** + +1. falls Juni zugeordnet kann die Contribution mit 500 GDD eingelöst werden +2. falls Juli zugeordnet muss die Contribution mit 500 GDD abgelehnt werden, da möglicher Schöpfungsbetrag überschritten + + +## Beispiel 2 + + +Ein User meldet sich in einer Zeitzone t0 + 1 an. Das bedeutet der User liegt 1 Stunde gegenüber der Backend-Zeit voraus. + +Konkret hat der User die Zeit 01.09.2022 00:20:00 auf dem Server ist aber die Zeit bei 31.08.2022 23:20:00 + +Für die Erstellung einer Contribution hat der User noch folgende Gültigkeitsmonate und Beträge zur Wahl: + + Juli 2022: 200 GDD | August 2022: 1000 GDD | September 2022: 1000 GDD + +**oder wird ihm** + +** + Juni 2022: 500 GDD | Juli 2022: 200 GDD | August 2022: 1000 GDD** + +**angezeigt, da auf dem BE noch der 31.08.2022 ist?** + +Er möchte für den September 2022 eine Contribution mit 500 GDD erfassen und wählt dabei im FE im Kalender den 01.09.2022. Dann liefert das FE folgende Contribution-Daten an das Backend: + +* Gültigkeitsdatum: 01.09.2022 00:00:00 (siehe Logauszüge der Fehleranalyse im Ticket #2179) +* Memo: text +* Betrag: 500 GDD +* **Zeitzone: wird eine Zeitzone des User aus dem Context geliefert?** + +Im Backend wird dieses dann interpretiert und verarbeitet mit: + +* Belegung des Schöpfungsmonate-Arrays: [ 6, 7, 8] **wie kann der User dann aber vorher September 2022 für die Schöpfung auswählen?** +* Gültigkeitsdatum: 01.09.2022 00:00:00 +* Memo: text +* Betrag 500 GDD +* created_at: 31.08.2022 23:20:00 + +Es kommt zu einem **Fehler im Backend**, da im Schöpfungsmonate-Array kein September (9) vorhanden ist, da auf dem Server noch der 31.08.2022 und damit das Array nur die Monate Juni, Juli, August und nicht September beinhaltet. + + +## Erkenntnisse: + +* die dem User angezeigten Schöpfungsmonate errechnen sich aus der lokalen User-Zeit und nicht aus der Backend-Zeit + * das Backend muss somit für Ermittlung der möglichen Schöpfungsmonate und deren noch freien Schöpfungssummen den UserTimeOffset berücksichten +* der gewählte Schöpfungsmonat muss 1:1 vom Frontend in das Backend übertragen werden +* es darf kein Mapping in die Backend-Zeit erfolgen + * sondern es muss der jeweilige UserTimeOffset mitgespeichert werden +* die Logik im BE muss den übertragenen bzw. ermittelten Offset der FE-Zeit entsprechend berücksichten und nicht die Backendzeit in der Logik anwenden + * im BE darf es kein einfaches now = new Date() geben + * im BE muss stattdessen ein userNow = new Date() + UserTimeOffset verwendet werden +* ein CreatedAt / UpdatedAt / DeletedAt / ConfirmedAt wird wie bisher in BE-Zeit gespeichert + * **NEIN nicht notwendig:** plus in einer jeweils neuen Spalte CreatedOffset / UpdatedOffset / DeletedOffset / ConfirmedOffset der dabei gültige UserTimeOffset +* im FE wird immer im Request-Header der aktuelle Zeitpunkt mit Zeitzone geschrieben +* + +## Entscheidung diff --git a/docu/Concepts/BusinessRequirements/graphics/ZeitzonenKonstellationen.drawio b/docu/Concepts/BusinessRequirements/graphics/ZeitzonenKonstellationen.drawio new file mode 100644 index 000000000..52ff19af9 --- /dev/null +++ b/docu/Concepts/BusinessRequirements/graphics/ZeitzonenKonstellationen.drawio @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/Concepts/BusinessRequirements/image/ZeitzonenKonstellationen.png b/docu/Concepts/BusinessRequirements/image/ZeitzonenKonstellationen.png new file mode 100644 index 0000000000000000000000000000000000000000..3f8fddbcb9ae0f29a748e90a851e39faf730b9e7 GIT binary patch literal 47231 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 literal 0 HcmV?d00001 From b024956dbdc871d6b3c5b7dbab263133fc536c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 14 Sep 2022 21:56:19 +0200 Subject: [PATCH 02/66] results of architecture-meeting and results of resolver method analysis to take care about user timezone offset --- .../Zeitzonen_Behandlung.md | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) diff --git a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md index cc681c33a..64d8ffbc4 100644 --- a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md +++ b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md @@ -93,3 +93,288 @@ Es kommt zu einem **Fehler im Backend**, da im Schöpfungsmonate-Array kein Sept * ## Entscheidung + +* in den HTTP-Request-Header wird generell der aktuelle Timestamp des Clients eingetragen, sodass die aktuelle Uhrzeit des Users ohne weitere Signatur-Änderungen in jedem Aufruf am Backend ankommt. Moritz erstellt Ticket +* es wird eine Analyse aller Backend-Aufrufe gemacht, die die Auswertung der User-Time und dessen evtl. Timezone-Differenz in der Logik des Backend-Aufrufs benötigt. +* diese Backend-Methoden müssen fachlich so überarbeitet werden, dass immer aus dem Timezone-Offset die korrekte fachliche Logik als Ergebnis heraus kommt. In der Datanbank wird aber immer die UTC-Zeit gespeichert. +* Es werden keine zusätzlichen Datanbank-Attribute zur Speicherung des User-TimeOffsets benötigt. + + +## Analyse der Backend-Aufrufe + +Es werden alle Resolver und ihre Methoden sowie im Resolver exportierte Attribute/Methoden untersucht. + +Mit + gekennzeichnet sind diese, die mit dem UserTimeOffset interagieren und überarbeitet werden müssen. + +Mit - gekennzeichnet sind diese, die keiner weiteren Aktion bedürfen. + + +### AdminResolver + +#### + adminCreateContribution + +Hier wird der User zur übergebenen Email inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt. + +Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln. + +Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden. + +#### - adminCreateContributionMessage + +nothing to do + +#### + adminCreateContributions + +Hier wird eine Liste von übergebenen Contributions über den internen Aufruf von *adminCreateContribution()* verarbeitet. Da dort eine Berücksichtigung des User-TimeOffsets notwendig ist, muss hier die UserTime entsprechen im Context weitergereicht werden. + +#### - adminDeleteContribution + +nothing to do + +#### + adminUpdateContribution + +analog adminCreateContribution() muss hier der User-TimeOffset berücksichtigt werden. + +#### + confirmContribution + +Hier wird intern *getUserCreation()* und *validateContribution()* aufgerufen, daher analog adminCreateContribution() + +#### + createContributionLink + +Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren. + +#### - creationTransactionList + +nothing to do + +#### - deleteContributionLink + +Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein. + +#### - deleteUser + +Es wird zwar der *deletedAt*-Zeitpunkt als Rückgabewert geliefert, doch m.E. dürft hier keine Berücksichtigung des User-TimezoneOffsets notwendig sein. + +#### - listContributionLinks + +nothing to do + +#### + listTransactionLinksAdmin + +Hier wird die BE-Zeit für die Suche nach ValidUntil verwendet. Dies sollte nocheinmal verifiziert werden. + +#### + listUnconfirmedContributions + +Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen. + +#### + searchUsers + +Hier wird intern *getUserCreations()* aufgerufen für die Summen der drei Schöpfungsmonate, somit ist der User-TimezoneOffset zu berücksichtigen. + +#### - sendActivationEmail + +analog *UserResolver.checkOptInCode* + +#### - setUserRole + +nothing to do + +#### - unDeleteUser + +nothing to do + +#### + updateContributionLink + +Hier werden zwar ein *ValidFrom* und ein *ValidTo* Datum übergeben, doch dürften diese keiner Beachtung eines User-TimezoneOffsets unterliegen. Trotzdem bitte noch einmal verifizieren. + + +### BalanceResolver + +#### + balance + +Hier wird der aktuelle Zeitpunkt des BE verwendet, um den Decay und die Summen der Kontostände zu ermitteln. Dies müsste eigentlich von dem User-TimezoneOffset unabhängig sein. Sollte aber noch einmal dahingehend verifiziert werden. + + +### CommunityResolver + +#### - communities + +nothing to do + +#### - getCommunityInfo + +nothing to do + + +### ContributionMessageResolver + +#### - createContributionMessage + +nothing to do + +#### - listContributionMessages + +nothing to do + + +### ContributionResolver + +#### + createContribution + +Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt. + +Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln. + +Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und der Initialisierung der Contribution berücksichtigt werden. + +#### - deleteContribution + +nothing to do + +#### - listAllContributions + +nothing to do + +#### - listContributions + +nothing to do + +#### + updateContribution + +Hier werden die Contributions des Users inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contributions, egal ob bestätigt oder noch offen ermittelt. + +Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln. + +Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und dem Update der Contribution berücksichtigt werden. + + +### GdtResolver + +#### - existPid + +nothing to do + +#### - gdtBalance + +nothing to do + +#### - listGDTEntries + +nothing to do + + +### KlicktippResolver + +nothing to do + + +### StatisticsResolver + +#### + communityStatistics + +Hier werden die Daten zum aktuellen BE-Zeitpunkt ermittelt und dem User angezeigt. Aber der User hat ggf. einen anderen TimeOffset. Daher die Frage, ob die Ermittlung der Statistik-Daten mit dem User-TimeOffset stattfinden muss. + + +### TransactionLinkResolver + +#### - transactionLinkCode + +nothing to do + +#### - transactionLinkExpireDate + +nothing to do + +#### - createTransactionLink + +nothing to do + +#### - deleteTransactionLink + +nothing to do + +#### - listTransactionLinks + +nothing to do + +#### - queryTransactionLink + +nothing to do + +#### - redeemTransactionLink + +nothing to do + + +### TransactionResolver + +#### - executeTransaction + +nothing to do + +#### - sendCoins + +nothing to do + +#### + transactionList + +Hier wird der aktuelle BE-Zeitpunkt verwendet, um die Summen der vorhandenen Transactions bis zu diesem Zeitpunkt zu ermitteln. Nach ersten Einschätzungen dürfte es hier nichts zu tun geben. Aber es sollte noch einmal geprüft werden. + + +### UserResolver + +#### - activationLink + +nothing to do + +#### - checkOptInCode + +Hier wird der übergebene OptIn-Code geprüft, ob schon wieder eine erneute Email gesendet werden kann. Die Zeiten werden auf reiner BE-Zeit verglichen, von daher gibt es hier nichts zu tun. + +#### - createUser + +nothing to do + +#### - forgotPassword + +In dieser Methode wird am Ende in der Methode *sendResetPasswordEmailMailer()* die Zeit berechnet, wie lange der OptIn-Code im Link gültig ist, default 1440 min oder 24 h. + +Es ist keine User-TimeOffset zu berücksichten, da der OptInCode direkt als Parameter im Aufruf von queryOptIn verwendet und dann dort mit der BE-Time verglichen wird. + +#### - hasElopage + +nothing to do + +#### - login + +nothing to do + +#### - logout + +nothing to do + +#### - queryOptIn + +Hier wird der OptIn-Code aus der *sendResetPasswordEmailMailer()* als Parameter geliefert. Da dessen Gültigkeit zuvor in forgotPassword mit der BE-Zeit gesetzt wurde, benögt man hier keine Berücksichtigung des User-TimeOffsets. + +#### - searchAdminUsers + +nothing to do + +#### - setPassword + +nothing to do, analog *queryOptIn* + +#### - printTimeDuration + +nothing to do + +#### - updateUserInfos + +nothing to do + +#### + verifyLogin + +Hier wird der User inklusive der Summen über die letzten drei Schöpfungsmonate aus seinen vorhandenen Contribtutions, egal ob bestätigt oder noch offen ermittelt. + +Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Schöpfungsmonate und dann daraus die korrekten Beträge der Contributions zu ermitteln. From 8caf482943ad3cdc9fa30a82ddda339597afb5f1 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 10 Nov 2022 10:52:36 +0100 Subject: [PATCH 03/66] Store transactionLinkId to the transaction --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 74c531c54..a3156497a 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -300,6 +300,7 @@ export class TransactionLinkResolver { transaction.balanceDate = now transaction.decay = decay ? decay.decay : new Decimal(0) transaction.decayStart = decay ? decay.start : null + transaction.transactionLinkId = contributionLink.id await queryRunner.manager.insert(DbTransaction, transaction) contribution.confirmedAt = now From 69af52f21a0c4a3b1ed0e2201f55be63145efacd Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 10 Nov 2022 10:53:08 +0100 Subject: [PATCH 04/66] Show via link for creations. --- .../TransactionRows/AmountAndNameRow.vue | 22 +++++++++---------- .../Transactions/TransactionCreation.vue | 11 +++++++++- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/TransactionRows/AmountAndNameRow.vue b/frontend/src/components/TransactionRows/AmountAndNameRow.vue index 322ad7dfa..96a31dcf3 100644 --- a/frontend/src/components/TransactionRows/AmountAndNameRow.vue +++ b/frontend/src/components/TransactionRows/AmountAndNameRow.vue @@ -10,21 +10,21 @@
-
+ {{ itemText }} - - {{ $t('via_link') }} - - -
+ {{ itemText }} + + {{ $t('via_link') }} + +
diff --git a/frontend/src/components/Transactions/TransactionCreation.vue b/frontend/src/components/Transactions/TransactionCreation.vue index 694d907ed..a832b9b1a 100644 --- a/frontend/src/components/Transactions/TransactionCreation.vue +++ b/frontend/src/components/Transactions/TransactionCreation.vue @@ -12,7 +12,12 @@ - + @@ -77,6 +82,10 @@ export default { type: String, required: true, }, + transactionLinkId: { + type: Number, + required: false, + }, previousBookedBalance: { type: String, required: true, From 369752783cc07685a9e73bbc710d86ee9a1ce14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 17 Nov 2022 13:46:59 +0100 Subject: [PATCH 05/66] Translate and HTML'lize email 'sendAccountActivation' --- .../emails/accountMultiRegistration/html.pug | 8 +++---- .../src/emails/sendAccountActivation/html.pug | 20 ++++++++++++++++ .../emails/sendAccountActivation/subject.pug | 1 + backend/src/emails/sendEmailTranslated.ts | 4 ++-- backend/src/emails/sendEmailVariants.ts | 24 ++++++++++++++++++- backend/src/graphql/resolver/UserResolver.ts | 11 +++++---- backend/src/locales/de.json | 13 ++++++++-- backend/src/locales/en.json | 21 +++++++++++----- 8 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 backend/src/emails/sendAccountActivation/html.pug create mode 100644 backend/src/emails/sendAccountActivation/subject.pug diff --git a/backend/src/emails/accountMultiRegistration/html.pug b/backend/src/emails/accountMultiRegistration/html.pug index e285c940b..b3764403b 100644 --- a/backend/src/emails/accountMultiRegistration/html.pug +++ b/backend/src/emails/accountMultiRegistration/html.pug @@ -1,11 +1,11 @@ doctype html -html(lang="en") +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(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) p= t('emails.accountMultiRegistration.emailReused') br span= t('emails.accountMultiRegistration.emailExists') @@ -17,6 +17,6 @@ html(lang="en") 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') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') br - span= t('emails.accountMultiRegistration.yourGradidoTeam') + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/sendAccountActivation/html.pug b/backend/src/emails/sendAccountActivation/html.pug new file mode 100644 index 000000000..18bce36b5 --- /dev/null +++ b/backend/src/emails/sendAccountActivation/html.pug @@ -0,0 +1,20 @@ +doctype html +html(lang=locale) + head + title= t('emails.sendAccountActivation.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.sendAccountActivation.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.sendAccountActivation.emailRegistered') + p= t('emails.sendAccountActivation.pleaseClickLink') + br + a(href=activationLink) #{activationLink} + br + span= t('emails.sendAccountActivation.orCopyLink') + p= t('emails.sendAccountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) + br + a(href=resendLink) #{resendLink} + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/sendAccountActivation/subject.pug b/backend/src/emails/sendAccountActivation/subject.pug new file mode 100644 index 000000000..09586310f --- /dev/null +++ b/backend/src/emails/sendAccountActivation/subject.pug @@ -0,0 +1 @@ += t('emails.sendAccountActivation.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 3fe4177f4..fc1161b8a 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -12,7 +12,7 @@ export const sendEmailTranslated = async (params: { cc?: string } template: string - locals: Record + locals: Record }): Promise | null> => { let resultSend: Record | null = null @@ -50,7 +50,7 @@ export const sendEmailTranslated = async (params: { }, }) - i18n.setLocale(params.locals.locale) // for email + i18n.setLocale(params.locals.locale as string) // for email // TESTING: see 'README.md' const email = new Email({ diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index fb142f206..3ee749354 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -11,9 +11,31 @@ export const sendAccountMultiRegistrationEmail = (data: { receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, template: 'accountMultiRegistration', locals: { - locale: data.language, firstName: data.firstName, lastName: data.lastName, + locale: data.language, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) +} + +export const sendAccountActivationEmail = (data: { + firstName: string + lastName: string + email: string + language: string + activationLink: string + timeDurationObject: Record +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'sendAccountActivation', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + activationLink: data.activationLink, + timeDurationObject: data.timeDurationObject, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, }, }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 81d0bab0f..d9e7cefa7 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -18,8 +18,10 @@ import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { OptInType } from '@enum/OptInType' import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' +import { + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, +} from '@/emails/sendEmailVariants' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' import { hasElopageBuys } from '@/util/hasElopageBuys' @@ -543,11 +545,12 @@ export class UserResolver { // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ - link: activationLink, firstName, lastName, email, - duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), + language, + activationLink, + timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) eventSendConfirmEmail.userId = dbUser.id diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 6c270f148..2b97d19f0 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -3,13 +3,22 @@ "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.", + "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" + }, + "general": { + "helloName": "Hallo {firstName} {lastName}", "sincerelyYours": "Mit freundlichen Grüßen,", - "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail", "yourGradidoTeam": "dein Gradido-Team" + }, + "sendAccountActivation": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "emailRegistered": "Deine E-Mail-Adresse wurde soeben bei Gradido registriert.", + "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", + "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", + "subject": "Gradido: E-Mail Überprüfung" } } } \ No newline at end of file diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 7655aae6a..e9f43e416 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -1,15 +1,24 @@ { - "emails": { - "accountMultiRegistration": { + "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.", + "subject": "Gradido: Try To Register Again With Your Email" + }, + "general": { + "helloName": "Hello {firstName} {lastName}", "sincerelyYours": "Sincerely yours,", - "subject": "Gradido: Try To Register Again With Your Email", "yourGradidoTeam": "your Gradido team" - } - } + }, + "sendAccountActivation": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "emailRegistered": "Your email address has just been registered with Gradido.", + "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", + "orCopyLink": "or copy the link above into your browser window.", + "subject": "Gradido: Email Verification" + } + } } \ No newline at end of file From 0fdb3e4401686f52128ecb07209e0c1ee83f4a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 17 Nov 2022 15:27:07 +0100 Subject: [PATCH 06/66] Test 'sendAccountActivationEmail' --- backend/src/emails/sendEmailVariants.test.ts | 71 +++++++++++++++++++- backend/src/emails/sendEmailVariants.ts | 36 +++++----- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 4ac8221a7..017efc270 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -1,5 +1,5 @@ import CONFIG from '@/config' -import { sendAccountMultiRegistrationEmail } from './sendEmailVariants' +import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' CONFIG.EMAIL = true @@ -19,6 +19,75 @@ jest.mock('./sendEmailTranslated', () => { describe('sendEmailVariants', () => { let result: Record | null + describe('sendAccountActivationEmail', () => { + beforeAll(async () => { + result = await sendAccountActivationEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + activationLink: 'http://localhost/checkEmail/6627633878930542284', + timeDurationObject: { hours: 24, minutes: 0 }, + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'sendAccountActivation', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + activationLink: 'http://localhost/checkEmail/6627633878930542284', + timeDurationObject: { hours: 24, minutes: 0 }, + 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: Email Verification', + html: + expect.stringContaining('Gradido: Email Verification') && + expect.stringContaining('>Gradido: Email Verification') && + expect.stringContaining( + 'Your email address has just been registered with Gradido.', + ) && + expect.stringContaining( + 'Please click on this link to complete the registration and activate your Gradido account:', + ) && + expect.stringContaining( + 'http://localhost/checkEmail/6627633878930542284', + ) && + expect.stringContaining('or copy the link above into your browser window.') && + expect.stringContaining( + 'The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', + ) && + expect.stringContaining( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) && + expect.stringContaining('Sincerely yours,
your Gradido team'), + text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'), + }), + }) + }) + }) + }) + describe('sendAccountMultiRegistrationEmail', () => { beforeAll(async () => { result = await sendAccountMultiRegistrationEmail({ diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 3ee749354..c507f6ace 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,24 +1,6 @@ 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: { - firstName: data.firstName, - lastName: data.lastName, - locale: data.language, - resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, - }, - }) -} - export const sendAccountActivationEmail = (data: { firstName: string lastName: string @@ -40,3 +22,21 @@ export const sendAccountActivationEmail = (data: { }, }) } + +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: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) +} From c1b91f94564079cfcef1d40eab956e124dfebe0b Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Fri, 18 Nov 2022 00:41:50 +0100 Subject: [PATCH 07/66] Update docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md Co-authored-by: Ulf Gebhardt --- docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md index 64d8ffbc4..8f2a9e49a 100644 --- a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md +++ b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md @@ -96,7 +96,7 @@ Es kommt zu einem **Fehler im Backend**, da im Schöpfungsmonate-Array kein Sept * in den HTTP-Request-Header wird generell der aktuelle Timestamp des Clients eingetragen, sodass die aktuelle Uhrzeit des Users ohne weitere Signatur-Änderungen in jedem Aufruf am Backend ankommt. Moritz erstellt Ticket * es wird eine Analyse aller Backend-Aufrufe gemacht, die die Auswertung der User-Time und dessen evtl. Timezone-Differenz in der Logik des Backend-Aufrufs benötigt. -* diese Backend-Methoden müssen fachlich so überarbeitet werden, dass immer aus dem Timezone-Offset die korrekte fachliche Logik als Ergebnis heraus kommt. In der Datanbank wird aber immer die UTC-Zeit gespeichert. +* diese Backend-Methoden müssen fachlich so überarbeitet werden, dass immer aus dem Timezone-Offset die korrekte fachliche Logik als Ergebnis heraus kommt. In der Datenbank wird aber immer die UTC-Zeit gespeichert. * Es werden keine zusätzlichen Datanbank-Attribute zur Speicherung des User-TimeOffsets benötigt. From 560aed056aa97de953e38e2f8d0d6f0063a5a51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 12:20:51 +0100 Subject: [PATCH 08/66] Fix spelling and punctuation in German email translations --- backend/src/locales/de.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 2b97d19f0..78d9d50b3 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -2,20 +2,20 @@ "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.", + "emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.", "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.", "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" }, "general": { - "helloName": "Hallo {firstName} {lastName}", + "helloName": "Hallo {firstName} {lastName},", "sincerelyYours": "Mit freundlichen Grüßen,", "yourGradidoTeam": "dein Gradido-Team" }, "sendAccountActivation": { "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", - "emailRegistered": "Deine E-Mail-Adresse wurde soeben bei Gradido registriert.", + "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: E-Mail Überprüfung" From b78229b646311ec991421261e4f7ea4763ac357a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 12:22:25 +0100 Subject: [PATCH 09/66] Fix faulty email content tests --- backend/src/emails/sendEmailVariants.test.ts | 99 +++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 017efc270..698ba32d9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -17,7 +17,8 @@ jest.mock('./sendEmailTranslated', () => { }) describe('sendEmailVariants', () => { - let result: Record | null + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let result: any describe('sendAccountActivationEmail', () => { beforeAll(async () => { @@ -27,7 +28,7 @@ describe('sendEmailVariants', () => { email: 'peter@lustig.de', language: 'en', activationLink: 'http://localhost/checkEmail/6627633878930542284', - timeDurationObject: { hours: 24, minutes: 0 }, + timeDurationObject: { hours: 23, minutes: 30 }, }) }) @@ -43,7 +44,7 @@ describe('sendEmailVariants', () => { lastName: 'Lustig', locale: 'en', activationLink: 'http://localhost/checkEmail/6627633878930542284', - timeDurationObject: { hours: 24, minutes: 0 }, + timeDurationObject: { hours: 23, minutes: 30 }, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, }, }) @@ -61,29 +62,32 @@ describe('sendEmailVariants', () => { from: 'Gradido (nicht antworten) ', attachments: [], subject: 'Gradido: Email Verification', - html: - expect.stringContaining('Gradido: Email Verification') && - expect.stringContaining('>Gradido: Email Verification') && - expect.stringContaining( - 'Your email address has just been registered with Gradido.', - ) && - expect.stringContaining( - 'Please click on this link to complete the registration and activate your Gradido account:', - ) && - expect.stringContaining( - 'http://localhost/checkEmail/6627633878930542284', - ) && - expect.stringContaining('or copy the link above into your browser window.') && - expect.stringContaining( - 'The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', - ) && - expect.stringContaining( - `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, - ) && - expect.stringContaining('Sincerely yours,
your Gradido team'), + html: expect.any(String), text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'), }), }) + expect(result.originalMessage.html).toContain('Gradido: Email Verification') + expect(result.originalMessage.html).toContain('>Gradido: Email Verification') + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Your email address has just been registered with Gradido.', + ) + expect(result.originalMessage.html).toContain( + 'Please click on this link to complete the registration and activate your Gradido account:', + ) + expect(result.originalMessage.html).toContain( + 'http://localhost/checkEmail/6627633878930542284', + ) + expect(result.originalMessage.html).toContain( + 'or copy the link above into your browser window.', + ) + expect(result.originalMessage.html).toContain( + 'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', + ) + expect(result.originalMessage.html).toContain( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) + expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') }) }) }) @@ -126,31 +130,36 @@ describe('sendEmailVariants', () => { 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'), + html: expect.any(String), text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'), }), }) + expect(result.originalMessage.html).toContain( + 'Gradido: Try To Register Again With Your Email', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Try To Register Again With Your Email', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Your email address has just been used again to register an account with Gradido.', + ) + expect(result.originalMessage.html).toContain( + 'However, an account already exists for your email address.', + ) + expect(result.originalMessage.html).toContain( + 'Please click on the following link if you have forgotten your password:', + ) + expect(result.originalMessage.html).toContain( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) + expect(result.originalMessage.html).toContain( + 'or copy the link above into your browser window.', + ) + expect(result.originalMessage.html).toContain( + 'If you are not the one who tried to register again, please contact our support:', + ) + expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') }) }) }) From 18a4408e8aca1c9bbbe39fedbcfd447b2cb26d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 13:37:29 +0100 Subject: [PATCH 10/66] Adjust the tests for new translatable 'sendAccountActivationEmail' --- .../graphql/resolver/AdminResolver.test.ts | 5 ++-- backend/src/graphql/resolver/AdminResolver.ts | 9 ++++---- .../src/graphql/resolver/UserResolver.test.ts | 23 +++++++++---------- backend/src/graphql/resolver/UserResolver.ts | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 503bab472..ea1bb848e 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -36,7 +36,7 @@ import { import { GraphQLError } from 'graphql' import { User } from '@entity/User' /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' import { Transaction as DbTransaction } from '@entity/Transaction' @@ -47,9 +47,10 @@ import { EventProtocolType } from '@/event/EventProtocolType' import { logger } from '@test/testSetup' // mock account activation email to avoid console spam -jest.mock('@/mailer/sendAccountActivationEmail', () => { +jest.mock('@/emails/sendEmailVariants', () => { return { __esModule: true, + // TODO: test the call of … sendAccountActivationEmail: jest.fn(), } }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 80c69a864..bd3ed20dc 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -39,8 +39,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' -import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver' -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +import { findUserByEmail, activationLink, getTimeDurationObject } from './UserResolver' +import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' import { @@ -656,11 +656,12 @@ export class AdminResolver { // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ - link: activationLink(emailContact.emailVerificationCode), firstName: user.firstName, lastName: user.lastName, email, - duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), + language: user.language, + activationLink: activationLink(emailContact.emailVerificationCode), + timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) // In case EMails are disabled log the activation link for the user diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 6323abfde..fac4618bf 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -18,15 +18,16 @@ import { verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queri import { GraphQLError } from 'graphql' import { User } from '@entity/User' import CONFIG from '@/config' -import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' +import { + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, +} from '@/emails/sendEmailVariants' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { printTimeDuration, activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' import { TransactionLink } from '@entity/TransactionLink' - import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocol } from '@entity/EventProtocol' import { logger, i18n as localization } from '@test/testSetup' @@ -39,16 +40,10 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' // import { klicktippSignIn } from '@/apis/KlicktippController' -jest.mock('@/mailer/sendAccountActivationEmail', () => { - return { - __esModule: true, - sendAccountActivationEmail: jest.fn(), - } -}) - jest.mock('@/emails/sendEmailVariants', () => { return { __esModule: true, + sendAccountActivationEmail: jest.fn(), sendAccountMultiRegistrationEmail: jest.fn(), } }) @@ -180,11 +175,15 @@ describe('UserResolver', () => { emailVerificationCode, ).replace(/{code}/g, '') expect(sendAccountActivationEmail).toBeCalledWith({ - link: activationLink, firstName: 'Peter', lastName: 'Lustig', email: 'peter@lustig.de', - duration: expect.any(String), + language: 'de', + activationLink, + timeDurationObject: expect.objectContaining({ + hours: expect.any(Number), + minutes: expect.any(Number), + }), }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 62636ebb7..23cf9eaeb 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -953,7 +953,7 @@ const canEmailResend = (updatedAt: Date): boolean => { return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) } -const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { +export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { if (time > 60) { return { hours: Math.floor(time / 60), From b8fdb59d73582ac5231a9dd8fe7cf1fcfbeb3661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 21 Nov 2022 13:38:18 +0100 Subject: [PATCH 11/66] Remove old email 'sendAccountActivationEmail' --- .../mailer/sendAccountActivationEmail.test.ts | 32 ------------------- .../src/mailer/sendAccountActivationEmail.ts | 17 ---------- backend/src/mailer/text/accountActivation.ts | 32 ------------------- .../mailer/text/accountMultiRegistration.ts | 25 --------------- 4 files changed, 106 deletions(-) delete mode 100644 backend/src/mailer/sendAccountActivationEmail.test.ts delete mode 100644 backend/src/mailer/sendAccountActivationEmail.ts delete mode 100644 backend/src/mailer/text/accountActivation.ts delete mode 100644 backend/src/mailer/text/accountMultiRegistration.ts diff --git a/backend/src/mailer/sendAccountActivationEmail.test.ts b/backend/src/mailer/sendAccountActivationEmail.test.ts deleted file mode 100644 index 08ddae166..000000000 --- a/backend/src/mailer/sendAccountActivationEmail.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { sendAccountActivationEmail } from './sendAccountActivationEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendAccountActivationEmail', () => { - beforeEach(async () => { - await sendAccountActivationEmail({ - link: 'activationLink', - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - duration: '23 hours and 30 minutes', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Peter Lustig `, - subject: 'Gradido: E-Mail Überprüfung', - text: - expect.stringContaining('Hallo Peter Lustig') && - expect.stringContaining('activationLink') && - expect.stringContaining('23 Stunden und 30 Minuten'), - }) - }) -}) diff --git a/backend/src/mailer/sendAccountActivationEmail.ts b/backend/src/mailer/sendAccountActivationEmail.ts deleted file mode 100644 index 335f80a82..000000000 --- a/backend/src/mailer/sendAccountActivationEmail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sendEMail } from './sendEMail' -import { accountActivation } from './text/accountActivation' -import CONFIG from '@/config' - -export const sendAccountActivationEmail = (data: { - link: string - firstName: string - lastName: string - email: string - duration: string -}): Promise => { - return sendEMail({ - to: `${data.firstName} ${data.lastName} <${data.email}>`, - subject: accountActivation.de.subject, - text: accountActivation.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }), - }) -} diff --git a/backend/src/mailer/text/accountActivation.ts b/backend/src/mailer/text/accountActivation.ts deleted file mode 100644 index 2755c4c0a..000000000 --- a/backend/src/mailer/text/accountActivation.ts +++ /dev/null @@ -1,32 +0,0 @@ -export const accountActivation = { - de: { - subject: 'Gradido: E-Mail Überprüfung', - text: (data: { - link: string - firstName: string - lastName: string - email: string - duration: string - resendLink: string - }): string => - `Hallo ${data.firstName} ${data.lastName}, - -Deine E-Mail-Adresse wurde soeben bei Gradido registriert. - -Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren: -${data.link} -oder kopiere den obigen Link in dein Browserfenster. - -Der Link hat eine Gültigkeit von ${data.duration - .replace('hours', 'Stunden') - .replace('minutes', 'Minuten') - .replace( - ' and ', - ' und ', - )}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst: -${data.resendLink} - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} diff --git a/backend/src/mailer/text/accountMultiRegistration.ts b/backend/src/mailer/text/accountMultiRegistration.ts deleted file mode 100644 index c5b55bac5..000000000 --- a/backend/src/mailer/text/accountMultiRegistration.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const accountMultiRegistration = { - de: { - subject: 'Gradido: Erneuter Registrierungsversuch mit deiner E-Mail', - text: (data: { - firstName: string - lastName: string - email: string - resendLink: string - }): string => - `Hallo ${data.firstName} ${data.lastName}, - -Deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren. -Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto. - -Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest: -${data.resendLink} -oder kopiere den obigen Link in dein Browserfenster. - -Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support: -https://gradido.net/de/contact/ - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} From 624cd6e4bc25d9ef3da04b933a622bbd2a2d028c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 08:34:48 +0100 Subject: [PATCH 12/66] Fix spelling of error message in 'sendActivationEmail' --- backend/src/graphql/resolver/AdminResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index bd3ed20dc..047f9bc4e 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -650,8 +650,8 @@ export class AdminResolver { } const emailContact = user.emailContact if (emailContact.deletedAt) { - logger.error(`The emailContact: ${email} of htis User is deleted.`) - throw new Error(`The emailContact: ${email} of htis User is deleted.`) + logger.error(`The emailContact: ${email} of this User is deleted.`) + throw new Error(`The emailContact: ${email} of this User is deleted.`) } // eslint-disable-next-line @typescript-eslint/no-unused-vars From 0695ac282b89ade1217f88b2de2ba0d388838891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 11:17:26 +0100 Subject: [PATCH 13/66] Adjust the fixed 'CONFIG.EMAIL_TEST_MODUS' problem in 'sendEmailTranslated.ts' --- backend/src/emails/sendEmailTranslated.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index fc1161b8a..39291a0ac 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -32,8 +32,7 @@ export const sendEmailTranslated = async (params: { 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) { + if (CONFIG.EMAIL_TEST_MODUS) { logger.info( `Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, ) From 53b29db25ec5388d733ca9f22ef8fd2ef9420372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 11:24:13 +0100 Subject: [PATCH 14/66] =?UTF-8?q?Add=20README's=20for=20locales=20?= =?UTF-8?q?=E2=80=93=20especially=20for=20quotation=20marks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/locales/README.md | 3 +++ backend/src/locales/README.md | 3 +++ frontend/src/locales/README.md | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 admin/src/locales/README.md create mode 100644 backend/src/locales/README.md create mode 100644 frontend/src/locales/README.md diff --git a/admin/src/locales/README.md b/admin/src/locales/README.md new file mode 100644 index 000000000..5d6bf75b1 --- /dev/null +++ b/admin/src/locales/README.md @@ -0,0 +1,3 @@ +# Localizations + +Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/backend/src/locales/README.md b/backend/src/locales/README.md new file mode 100644 index 000000000..5d6bf75b1 --- /dev/null +++ b/backend/src/locales/README.md @@ -0,0 +1,3 @@ +# Localizations + +Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/frontend/src/locales/README.md b/frontend/src/locales/README.md new file mode 100644 index 000000000..2c03abbd4 --- /dev/null +++ b/frontend/src/locales/README.md @@ -0,0 +1,25 @@ +# Localizations + +## Quotation Marks + +The following characters are different from the programming quotation mark: + +`"` + +### English + +In English, we use these double-barreled quotation marks: + +“This is a sample sentence.” + +Please copy and paste … + +See + +### German + +In German, we use these double-barreled quotation marks: + +„Dies ist ein Beispielsatz.“ + +Please copy and paste … From 76fa42552931783b7bd819e787070f6fd3d427df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 13:18:32 +0100 Subject: [PATCH 15/66] Refactor 'sendAddedContributionMessageEmail' email to HTML and translatable --- backend/src/emails/accountActivation/html.pug | 20 +++++++++++++ .../src/emails/accountActivation/subject.pug | 1 + .../emails/addedContributionMessage/html.pug | 17 +++++++++++ .../addedContributionMessage/subject.pug | 1 + .../src/emails/sendAccountActivation/html.pug | 20 ------------- .../emails/sendAccountActivation/subject.pug | 1 - backend/src/emails/sendEmailVariants.test.ts | 2 +- backend/src/emails/sendEmailVariants.ts | 28 ++++++++++++++++++- backend/src/graphql/resolver/AdminResolver.ts | 16 +++++------ backend/src/locales/de.json | 23 +++++++++------ backend/src/locales/en.json | 23 +++++++++------ 11 files changed, 105 insertions(+), 47 deletions(-) create mode 100644 backend/src/emails/accountActivation/html.pug create mode 100644 backend/src/emails/accountActivation/subject.pug create mode 100644 backend/src/emails/addedContributionMessage/html.pug create mode 100644 backend/src/emails/addedContributionMessage/subject.pug delete mode 100644 backend/src/emails/sendAccountActivation/html.pug delete mode 100644 backend/src/emails/sendAccountActivation/subject.pug diff --git a/backend/src/emails/accountActivation/html.pug b/backend/src/emails/accountActivation/html.pug new file mode 100644 index 000000000..9c631c960 --- /dev/null +++ b/backend/src/emails/accountActivation/html.pug @@ -0,0 +1,20 @@ +doctype html +html(lang=locale) + head + title= t('emails.accountActivation.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.accountActivation.emailRegistered') + p= t('emails.accountActivation.pleaseClickLink') + br + a(href=activationLink) #{activationLink} + br + span= t('emails.accountActivation.orCopyLink') + p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) + br + a(href=resendLink) #{resendLink} + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/accountActivation/subject.pug b/backend/src/emails/accountActivation/subject.pug new file mode 100644 index 000000000..378053bbf --- /dev/null +++ b/backend/src/emails/accountActivation/subject.pug @@ -0,0 +1 @@ += t('emails.accountActivation.subject') \ No newline at end of file diff --git a/backend/src/emails/addedContributionMessage/html.pug b/backend/src/emails/addedContributionMessage/html.pug new file mode 100644 index 000000000..020f36c33 --- /dev/null +++ b/backend/src/emails/addedContributionMessage/html.pug @@ -0,0 +1,17 @@ +doctype html +html(lang=locale) + head + title= t('emails.addedContributionMessage.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.addedContributionMessage.toSeeAndAnswerMessage') + p= t('emails.addedContributionMessage.linkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.addedContributionMessage.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/addedContributionMessage/subject.pug b/backend/src/emails/addedContributionMessage/subject.pug new file mode 100644 index 000000000..8620725f8 --- /dev/null +++ b/backend/src/emails/addedContributionMessage/subject.pug @@ -0,0 +1 @@ += t('emails.addedContributionMessage.subject') \ No newline at end of file diff --git a/backend/src/emails/sendAccountActivation/html.pug b/backend/src/emails/sendAccountActivation/html.pug deleted file mode 100644 index 18bce36b5..000000000 --- a/backend/src/emails/sendAccountActivation/html.pug +++ /dev/null @@ -1,20 +0,0 @@ -doctype html -html(lang=locale) - head - title= t('emails.sendAccountActivation.subject') - body - h1(style='margin-bottom: 24px;')= t('emails.sendAccountActivation.subject') - #container.col - p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) - p= t('emails.sendAccountActivation.emailRegistered') - p= t('emails.sendAccountActivation.pleaseClickLink') - br - a(href=activationLink) #{activationLink} - br - span= t('emails.sendAccountActivation.orCopyLink') - p= t('emails.sendAccountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) - br - a(href=resendLink) #{resendLink} - p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') - br - span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/sendAccountActivation/subject.pug b/backend/src/emails/sendAccountActivation/subject.pug deleted file mode 100644 index 09586310f..000000000 --- a/backend/src/emails/sendAccountActivation/subject.pug +++ /dev/null @@ -1 +0,0 @@ -= t('emails.sendAccountActivation.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 698ba32d9..4b8e184ae 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -38,7 +38,7 @@ describe('sendEmailVariants', () => { receiver: { to: 'Peter Lustig ', }, - template: 'sendAccountActivation', + template: 'accountActivation', locals: { firstName: 'Peter', lastName: 'Lustig', diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index c507f6ace..4d0cd6e5c 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,6 +1,32 @@ import CONFIG from '@/config' import { sendEmailTranslated } from './sendEmailTranslated' +export const sendAddedContributionMessageEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { + to: `${data.firstName} ${data.lastName} <${data.email}>`, + }, + template: 'addedContributionMessage', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} + export const sendAccountActivationEmail = (data: { firstName: string lastName: string @@ -11,7 +37,7 @@ export const sendAccountActivationEmail = (data: { }): Promise | null> => { return sendEmailTranslated({ receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, - template: 'sendAccountActivation', + template: 'accountActivation', locals: { firstName: data.firstName, lastName: data.lastName, diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 047f9bc4e..1c1e8968b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -40,7 +40,10 @@ import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' import { findUserByEmail, activationLink, getTimeDurationObject } from './UserResolver' -import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' +import { + sendAddedContributionMessageEmail, + sendAccountActivationEmail, +} from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' import { @@ -65,7 +68,6 @@ import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail' -import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Event, @@ -896,15 +898,13 @@ export class AdminResolver { } await sendAddedContributionMessageEmail({ + firstName: contribution.user.firstName, + lastName: contribution.user.lastName, + email: contribution.user.emailContact.email, + language: contribution.user.language, senderFirstName: user.firstName, senderLastName: user.lastName, - recipientFirstName: contribution.user.firstName, - recipientLastName: contribution.user.lastName, - recipientEmail: contribution.user.emailContact.email, - senderEmail: user.emailContact.email, contributionMemo: contribution.memo, - message, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) await queryRunner.commitTransaction() } catch (e) { diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 78d9d50b3..85bac73c8 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -1,5 +1,19 @@ { "emails": { + "addedContributionMessage": { + "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", + "linkToYourAccount": "Link zu deinem Konto:", + "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", + "subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag", + "toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" + }, + "accountActivation": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", + "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", + "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", + "subject": "Gradido: E-Mail Überprüfung" + }, "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.", @@ -10,15 +24,8 @@ }, "general": { "helloName": "Hallo {firstName} {lastName},", - "sincerelyYours": "Mit freundlichen Grüßen,", + "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" - }, - "sendAccountActivation": { - "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", - "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", - "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", - "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", - "subject": "Gradido: E-Mail Überprüfung" } } } \ No newline at end of file diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index e9f43e416..5207696da 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -1,5 +1,19 @@ { "emails": { + "addedContributionMessage": { + "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", + "linkToYourAccount": "Link to your account:", + "pleaseDoNotReply": "Please do not reply to this email!", + "subject": "Gradido: Message about your common good contribution", + "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!" + }, + "accountActivation": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "emailRegistered": "Your email address has just been registered with Gradido.", + "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", + "orCopyLink": "or copy the link above into your browser window.", + "subject": "Gradido: Email Verification" + }, "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.", @@ -10,15 +24,8 @@ }, "general": { "helloName": "Hello {firstName} {lastName}", - "sincerelyYours": "Sincerely yours,", + "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" - }, - "sendAccountActivation": { - "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", - "emailRegistered": "Your email address has just been registered with Gradido.", - "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", - "orCopyLink": "or copy the link above into your browser window.", - "subject": "Gradido: Email Verification" } } } \ No newline at end of file From fb015671698b28b5d1d4f425c3e0888557707d55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 14:13:01 +0100 Subject: [PATCH 16/66] Test 'sendAddedContributionMessageEmail' in 'sendEmailVariants.test.ts' --- backend/src/emails/sendEmailVariants.test.ts | 86 +++++++++++++++++++- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 4b8e184ae..38e901828 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -1,5 +1,9 @@ import CONFIG from '@/config' -import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail } from './sendEmailVariants' +import { + sendAddedContributionMessageEmail, + sendAccountActivationEmail, + sendAccountMultiRegistrationEmail, +} from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' CONFIG.EMAIL = true @@ -20,6 +24,78 @@ describe('sendEmailVariants', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let result: any + describe('sendAddedContributionMessageEmail', () => { + beforeAll(async () => { + result = await sendAddedContributionMessageEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'addedContributionMessage', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + 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: Message about your common good contribution', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: MESSAGE ABOUT YOUR COMMON GOOD CONTRIBUTION'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Message about your common good contribution', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Message about your common good contribution', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.', + ) + expect(result.originalMessage.html).toContain( + 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!', + ) + expect(result.originalMessage.html).toContain( + 'Link to your account: http://localhost/overview', + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) + describe('sendAccountActivationEmail', () => { beforeAll(async () => { result = await sendAccountActivationEmail({ @@ -66,6 +142,8 @@ describe('sendEmailVariants', () => { text: expect.stringContaining('GRADIDO: EMAIL VERIFICATION'), }), }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') expect(result.originalMessage.html).toContain('Gradido: Email Verification') expect(result.originalMessage.html).toContain('>Gradido: Email Verification') expect(result.originalMessage.html).toContain('Hello Peter Lustig') @@ -87,7 +165,7 @@ describe('sendEmailVariants', () => { expect(result.originalMessage.html).toContain( `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, ) - expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') }) }) }) @@ -134,6 +212,8 @@ describe('sendEmailVariants', () => { text: expect.stringContaining('GRADIDO: TRY TO REGISTER AGAIN WITH YOUR EMAIL'), }), }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') expect(result.originalMessage.html).toContain( 'Gradido: Try To Register Again With Your Email', ) @@ -159,7 +239,7 @@ describe('sendEmailVariants', () => { expect(result.originalMessage.html).toContain( 'If you are not the one who tried to register again, please contact our support:', ) - expect(result.originalMessage.html).toContain('Sincerely yours,
your Gradido team') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') }) }) }) From a1ec8dfa4f8b2a477b2ce65949f4e082478e9a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 14:16:10 +0100 Subject: [PATCH 17/66] Remove old untranslated email 'sendAddedContributionMessageEmail' --- .../sendAddedContributionMessageEmail.test.ts | 40 ------------------- .../sendAddedContributionMessageEmail.ts | 26 ------------ .../text/contributionMessageReceived.ts | 28 ------------- 3 files changed, 94 deletions(-) delete mode 100644 backend/src/mailer/sendAddedContributionMessageEmail.test.ts delete mode 100644 backend/src/mailer/sendAddedContributionMessageEmail.ts delete mode 100644 backend/src/mailer/text/contributionMessageReceived.ts diff --git a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts b/backend/src/mailer/sendAddedContributionMessageEmail.test.ts deleted file mode 100644 index 9a2ec1aa1..000000000 --- a/backend/src/mailer/sendAddedContributionMessageEmail.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { sendAddedContributionMessageEmail } from './sendAddedContributionMessageEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendAddedContributionMessageEmail', () => { - beforeEach(async () => { - await sendAddedContributionMessageEmail({ - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - senderEmail: 'peter@lustig.de', - contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', - message: 'Was für ein Besen ist es geworden?', - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Bibi Bloxberg `, - subject: 'Nachricht zu deinem Gemeinwohl-Beitrag', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining('Peter Lustig') && - expect.stringContaining( - '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/sendAddedContributionMessageEmail.ts b/backend/src/mailer/sendAddedContributionMessageEmail.ts deleted file mode 100644 index 14d5f6d31..000000000 --- a/backend/src/mailer/sendAddedContributionMessageEmail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import { sendEMail } from './sendEMail' -import { contributionMessageReceived } from './text/contributionMessageReceived' - -export const sendAddedContributionMessageEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - senderEmail: string - contributionMemo: string - message: string - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>, - subject=${contributionMessageReceived.de.subject}, - text=${contributionMessageReceived.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`, - subject: contributionMessageReceived.de.subject, - text: contributionMessageReceived.de.text(data), - }) -} diff --git a/backend/src/mailer/text/contributionMessageReceived.ts b/backend/src/mailer/text/contributionMessageReceived.ts deleted file mode 100644 index 301ebef22..000000000 --- a/backend/src/mailer/text/contributionMessageReceived.ts +++ /dev/null @@ -1,28 +0,0 @@ -export const contributionMessageReceived = { - de: { - subject: 'Nachricht zu deinem Gemeinwohl-Beitrag', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - senderEmail: string - contributionMemo: string - message: string - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -du hast zu deinem Gemeinwohl-Beitrag "${data.contributionMemo}" eine Nachricht von ${data.senderFirstName} ${data.senderLastName} erhalten. - -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! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 98798d33228e35a0c93f4509345cc10b353ac89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 22 Nov 2022 17:24:36 +0100 Subject: [PATCH 18/66] Adjust test of 'sendAddedContributionMessageEmail' in 'ContributionMessageResolver.test.ts' --- .../graphql/resolver/AdminResolver.test.ts | 8 ++++--- .../ContributionMessageResolver.test.ts | 23 +++++++++++-------- .../src/graphql/resolver/UserResolver.test.ts | 10 +++++--- backend/test/testSetup.ts | 4 ++++ 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index ea1bb848e..2ff7b0437 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -3,6 +3,7 @@ import { objectValuesToArray } from '@/util/utilities' import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { creationFactory } from '@/seeds/factory/creation' import { creations } from '@/seeds/creation/index' @@ -44,14 +45,15 @@ import { ContributionLink as DbContributionLink } from '@entity/ContributionLink import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { EventProtocol } from '@entity/EventProtocol' import { EventProtocolType } from '@/event/EventProtocolType' -import { logger } from '@test/testSetup' // mock account activation email to avoid console spam jest.mock('@/emails/sendEmailVariants', () => { + const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { __esModule: true, + ...originalModule, // TODO: test the call of … - sendAccountActivationEmail: jest.fn(), + sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), } }) @@ -67,7 +69,7 @@ let mutate: any, query: any, con: any let testEnv: any beforeAll(async () => { - testEnv = await testEnvironment() + testEnv = await testEnvironment(logger, localization) mutate = testEnv.mutate query = testEnv.query con = testEnv.con diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 612c2d20b..436830c2c 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { GraphQLError } from 'graphql' import { adminCreateContributionMessage, @@ -13,12 +14,16 @@ import { listContributionMessages } from '@/seeds/graphql/queries' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' -import { sendAddedContributionMessageEmail } from '@/mailer/sendAddedContributionMessageEmail' +import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants' -jest.mock('@/mailer/sendAddedContributionMessageEmail', () => { +jest.mock('@/emails/sendEmailVariants', () => { + const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { __esModule: true, - sendAddedContributionMessageEmail: jest.fn(), + ...originalModule, + sendAddedContributionMessageEmail: jest.fn((a) => + originalModule.sendAddedContributionMessageEmail(a), + ), } }) @@ -27,7 +32,7 @@ let testEnv: any let result: any beforeAll(async () => { - testEnv = await testEnvironment() + testEnv = await testEnvironment(logger, localization) mutate = testEnv.mutate con = testEnv.con await cleanDB() @@ -162,15 +167,13 @@ describe('ContributionMessageResolver', () => { it('calls sendAddedContributionMessageEmail', async () => { expect(sendAddedContributionMessageEmail).toBeCalledWith({ + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + language: 'de', senderFirstName: 'Peter', senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - senderEmail: 'peter@lustig.de', contributionMemo: 'Test env contribution', - message: 'Admin Test', - overviewURL: 'http://localhost/overview', }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index fac4618bf..1303b8aaf 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { @@ -30,7 +31,6 @@ import { ContributionLink } from '@model/ContributionLink' import { TransactionLink } from '@entity/TransactionLink' import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocol } from '@entity/EventProtocol' -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' @@ -41,10 +41,14 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' // import { klicktippSignIn } from '@/apis/KlicktippController' jest.mock('@/emails/sendEmailVariants', () => { + const originalModule = jest.requireActual('@/emails/sendEmailVariants') return { __esModule: true, - sendAccountActivationEmail: jest.fn(), - sendAccountMultiRegistrationEmail: jest.fn(), + ...originalModule, + sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), + sendAccountMultiRegistrationEmail: jest.fn((a) => + originalModule.sendAccountMultiRegistrationEmail(a), + ), } }) diff --git a/backend/test/testSetup.ts b/backend/test/testSetup.ts index 06779674d..300a9cbf3 100644 --- a/backend/test/testSetup.ts +++ b/backend/test/testSetup.ts @@ -1,6 +1,10 @@ +import CONFIG from '@/config' import { backendLogger as logger } from '@/server/logger' import { i18n } from '@/server/localization' +CONFIG.EMAIL = true +CONFIG.EMAIL_TEST_MODUS = false + jest.setTimeout(1000000) jest.mock('@/server/logger', () => { From ea62a7f7100d62309b10a1a2a564671b301d7e7d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 22 Nov 2022 18:01:36 +0100 Subject: [PATCH 19/66] fix(backend): email verification code never expired --- .../graphql/resolver/EmailOptinCodes.test.ts | 125 ++++++++++++++++++ .../src/graphql/resolver/UserResolver.test.ts | 24 ++-- backend/src/graphql/resolver/UserResolver.ts | 106 +++------------ backend/test/helpers.ts | 5 +- 4 files changed, 157 insertions(+), 103 deletions(-) create mode 100644 backend/src/graphql/resolver/EmailOptinCodes.test.ts diff --git a/backend/src/graphql/resolver/EmailOptinCodes.test.ts b/backend/src/graphql/resolver/EmailOptinCodes.test.ts new file mode 100644 index 000000000..1cf22850d --- /dev/null +++ b/backend/src/graphql/resolver/EmailOptinCodes.test.ts @@ -0,0 +1,125 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { testEnvironment, cleanDB } from '@test/helpers' +import { User as DbUser } from '@entity/User' +import { createUser, setPassword, forgotPassword } from '@/seeds/graphql/mutations' +import { queryOptIn } from '@/seeds/graphql/queries' +import CONFIG from '@/config' +import { GraphQLError } from 'graphql' + +let mutate: any, query: any, con: any +let testEnv: any + +CONFIG.EMAIL_CODE_VALID_TIME = 1440 +CONFIG.EMAIL_CODE_REQUEST_TIME = 10 + +beforeAll(async () => { + testEnv = await testEnvironment() + mutate = testEnv.mutate + query = testEnv.query + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('EmailOptinCodes', () => { + let optinCode: string + beforeAll(async () => { + const variables = { + email: 'peter@lustig.de', + firstName: 'Peter', + lastName: 'Lustig', + language: 'de', + } + const { + data: { createUser: user }, + } = await mutate({ mutation: createUser, variables }) + const dbObject = await DbUser.findOneOrFail({ + where: { id: user.id }, + relations: ['emailContact'], + }) + optinCode = dbObject.emailContact.emailVerificationCode.toString() + }) + + describe('queryOptIn', () => { + it('has a valid optin code', async () => { + await expect( + query({ query: queryOptIn, variables: { optIn: optinCode } }), + ).resolves.toMatchObject({ + data: { + queryOptIn: true, + }, + errors: undefined, + }) + }) + + describe('run time forward until code must be expired', () => { + beforeAll(() => { + jest.useFakeTimers() + setTimeout(jest.fn(), CONFIG.EMAIL_CODE_VALID_TIME * 60 * 1000) + jest.runAllTimers() + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('throws an error', async () => { + await expect( + query({ query: queryOptIn, variables: { optIn: optinCode } }), + ).resolves.toMatchObject({ + data: null, + errors: [new GraphQLError('email was sent more than 24 hours ago')], + }) + }) + + it('does not allow to set password', async () => { + await expect( + mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }), + ).resolves.toMatchObject({ + data: null, + errors: [new GraphQLError('email was sent more than 24 hours ago')], + }) + }) + }) + }) + + describe('forgotPassword', () => { + it('throws an error', async () => { + await expect( + mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }), + ).resolves.toMatchObject({ + data: null, + errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')], + }) + }) + + describe('run time forward until code can be resent', () => { + beforeAll(() => { + jest.useFakeTimers() + setTimeout(jest.fn(), CONFIG.EMAIL_CODE_REQUEST_TIME * 60 * 1000) + jest.runAllTimers() + }) + + afterAll(() => { + jest.useRealTimers() + }) + + it('cann send email again', async () => { + await expect( + mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }), + ).resolves.toMatchObject({ + data: { + forgotPassword: true, + }, + errors: undefined, + }) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 6323abfde..c382d8bc2 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -21,7 +21,7 @@ import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { sendAccountMultiRegistrationEmail } from '@/emails/sendEmailVariants' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' -import { printTimeDuration, activationLink } from './UserResolver' +import { printTimeDuration } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' @@ -804,12 +804,8 @@ describe('UserResolver', () => { }) describe('user exists in DB', () => { - let emailContact: UserContact - beforeAll(async () => { await userFactory(testEnv, bibiBloxberg) - // await resetEntity(LoginEmailOptIn) - emailContact = await UserContact.findOneOrFail(variables) }) afterAll(async () => { @@ -818,7 +814,7 @@ describe('UserResolver', () => { }) describe('duration not expired', () => { - it('returns true', async () => { + it('throws an error', async () => { await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( expect.objectContaining({ errors: [ @@ -844,15 +840,15 @@ describe('UserResolver', () => { }), ) }) - }) - it('sends reset password email', () => { - expect(sendResetPasswordEmail).toBeCalledWith({ - link: activationLink(emailContact.emailVerificationCode), - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - duration: expect.any(String), + it('sends reset password email', () => { + expect(sendResetPasswordEmail).toBeCalledWith({ + link: expect.any(String), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + duration: expect.any(String), + }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 707b7ac49..e6a86bba5 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -149,16 +149,6 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[ return [encryptionKeyHash, encryptionKey] } -/* -const getEmailHash = (email: string): Buffer => { - logger.trace('getEmailHash...') - const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES) - sodium.crypto_generichash(emailHash, Buffer.from(email)) - logger.debug(`getEmailHash...successful: ${emailHash}`) - return emailHash -} -*/ - const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => { logger.trace('SecretKeyCryptographyEncrypt...') const encrypted = Buffer.alloc(message.length + sodium.crypto_secretbox_MACBYTES) @@ -194,89 +184,33 @@ const newEmailContact = (email: string, userId: number): DbUserContact => { logger.debug(`newEmailContact...successful: ${emailContact}`) return emailContact } -/* -const newEmailOptIn = (userId: number): LoginEmailOptIn => { - logger.trace('newEmailOptIn...') - const emailOptIn = new LoginEmailOptIn() - emailOptIn.verificationCode = random(64) - emailOptIn.userId = userId - emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER - logger.debug(`newEmailOptIn...successful: ${emailOptIn}`) - return emailOptIn -} -*/ -/* -// needed by AdminResolver -// checks if given code exists and can be resent -// if optIn does not exits, it is created -export const checkOptInCode = async ( - optInCode: LoginEmailOptIn | undefined, - user: DbUser, - optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, -): Promise => { - logger.info(`checkOptInCode... ${optInCode}`) - if (optInCode) { - if (!canResendOptIn(optInCode)) { - logger.error( - `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, - ) - throw new Error( - `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, - ) - } - optInCode.updatedAt = new Date() - optInCode.resendCount++ - } else { - logger.trace('create new OptIn for userId=' + user.id) - optInCode = newEmailOptIn(user.id) - } - if (user.emailChecked) { - optInCode.emailOptInTypeId = optInType - } - await LoginEmailOptIn.save(optInCode).catch(() => { - logger.error('Unable to save optin code= ' + optInCode) - throw new Error('Unable to save optin code.') - }) - logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${user.id}`) - return optInCode -} -*/ export const checkEmailVerificationCode = async ( emailContact: DbUserContact, optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, ): Promise => { logger.info(`checkEmailVerificationCode... ${emailContact}`) - if (emailContact.updatedAt) { - if (!canEmailResend(emailContact.updatedAt)) { - logger.error( - `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, - ) - throw new Error( - `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, - ) - } - emailContact.updatedAt = new Date() - emailContact.emailResendCount++ - } else { - logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId) - emailContact.emailChecked = false - emailContact.emailVerificationCode = random(64) + if (!canEmailResend(emailContact.updatedAt || emailContact.createdAt)) { + logger.error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) + throw new Error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) } + emailContact.updatedAt = new Date() + emailContact.emailResendCount++ + emailContact.emailVerificationCode = random(64) emailContact.emailOptInTypeId = optInType await DbUserContact.save(emailContact).catch(() => { logger.error('Unable to save email verification code= ' + emailContact) throw new Error('Unable to save email verification code.') }) - logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`) + logger.info(`checkEmailVerificationCode...successful: ${emailContact}`) return emailContact } @@ -384,6 +318,7 @@ export class UserResolver { @Authorized([RIGHTS.LOGOUT]) @Mutation(() => String) async logout(): Promise { + // TODO: Event still missing here!! // TODO: We dont need this anymore, but might need this in the future in oder to invalidate a valid JWT-Token. // Furthermore this hook can be useful for tracking user behaviour (did he logout or not? Warn him if he didn't on next login) // The functionality is fully client side - the client just needs to delete his token with the current implementation. @@ -657,7 +592,7 @@ export class UserResolver { }) logger.debug('userContact loaded...') // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isEmailVerificationCodeValid(userContact.updatedAt)) { + if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { logger.error( `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) @@ -760,7 +695,7 @@ export class UserResolver { const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: optIn }) logger.debug(`found optInCode=${userContact}`) // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes - if (!isEmailVerificationCodeValid(userContact.updatedAt)) { + if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) { logger.error( `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) @@ -935,10 +870,7 @@ const isOptInValid = (optIn: LoginEmailOptIn): boolean => { return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME) } */ -const isEmailVerificationCodeValid = (updatedAt: Date | null): boolean => { - if (updatedAt == null) { - return true - } +const isEmailVerificationCodeValid = (updatedAt: Date): boolean => { return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME) } /* diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts index 7ee8e6052..1935b01a0 100644 --- a/backend/test/helpers.ts +++ b/backend/test/helpers.ts @@ -5,6 +5,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../src/server/createServer' import { initialize } from '@dbTools/helpers' import { entities } from '@entity/index' +import { i18n, logger } from './testSetup' export const headerPushMock = jest.fn((t) => { context.token = t.value @@ -26,8 +27,8 @@ export const cleanDB = async () => { } } -export const testEnvironment = async (logger?: any, localization?: any) => { - const server = await createServer(context, logger, localization) +export const testEnvironment = async (testLogger: any = logger, testI18n: any = i18n) => { + const server = await createServer(context, testLogger, testI18n) const con = server.con const testClient = createTestClient(server.apollo) const mutate = testClient.mutate From a1e8d0b2061c631a4cf3d23e94ff96fa748d03c9 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 23 Nov 2022 17:57:22 +0100 Subject: [PATCH 20/66] Update docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md --- docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md index 8f2a9e49a..771526ffb 100644 --- a/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md +++ b/docu/Concepts/BusinessRequirements/Zeitzonen_Behandlung.md @@ -247,7 +247,6 @@ Hier muss der User-TimeOffset berücksichtigt werden, um die korrekten drei Sch Zusätzlich wird als Parameter ein *creationDate* vom User mitgeliefert, das dem User-TimeOffset unterliegt. Auch dieses muss entsprechend beachtet und beim internen Aufruf von *validateContribution()* und dem Update der Contribution berücksichtigt werden. - ### GdtResolver #### - existPid From c87ffd5b8a54d57ec9617961b1a8a9174c9d5714 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 14:44:50 +0100 Subject: [PATCH 21/66] Refactor 'sendContributionConfirmedEmail' email to HTML and translatable --- .../emails/addedContributionMessage/html.pug | 4 +-- .../src/emails/contributionConfirmed/html.pug | 17 ++++++++++ .../emails/contributionConfirmed/subject.pug | 1 + backend/src/emails/sendEmailTranslated.ts | 3 +- backend/src/emails/sendEmailVariants.ts | 34 +++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 10 +++--- backend/src/locales/de.json | 10 ++++-- backend/src/locales/en.json | 10 ++++-- 8 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 backend/src/emails/contributionConfirmed/html.pug create mode 100644 backend/src/emails/contributionConfirmed/subject.pug diff --git a/backend/src/emails/addedContributionMessage/html.pug b/backend/src/emails/addedContributionMessage/html.pug index 020f36c33..5e5d0975c 100644 --- a/backend/src/emails/addedContributionMessage/html.pug +++ b/backend/src/emails/addedContributionMessage/html.pug @@ -8,10 +8,10 @@ html(lang=locale) p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo }) p= t('emails.addedContributionMessage.toSeeAndAnswerMessage') - p= t('emails.addedContributionMessage.linkToYourAccount') + p= t('emails.general.linkToYourAccount') span= " " a(href=overviewURL) #{overviewURL} - p= t('emails.addedContributionMessage.pleaseDoNotReply') + p= t('emails.general.pleaseDoNotReply') p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') br span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/contributionConfirmed/html.pug b/backend/src/emails/contributionConfirmed/html.pug new file mode 100644 index 000000000..e60e6c700 --- /dev/null +++ b/backend/src/emails/contributionConfirmed/html.pug @@ -0,0 +1,17 @@ +doctype html +html(lang=locale) + head + title= t('emails.contributionConfirmed.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.contributionConfirmed.contributionAmount', { contributionAmount }) + p= t('emails.general.linkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/contributionConfirmed/subject.pug b/backend/src/emails/contributionConfirmed/subject.pug new file mode 100644 index 000000000..7e74a77c6 --- /dev/null +++ b/backend/src/emails/contributionConfirmed/subject.pug @@ -0,0 +1 @@ += t('emails.contributionConfirmed.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 39291a0ac..9468e9f97 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -1,11 +1,10 @@ +import CONFIG from '@/config' 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 diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 4d0cd6e5c..8d1dcf8de 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,3 +1,5 @@ +import i18n from 'i18n' +import Decimal from 'decimal.js-light' import CONFIG from '@/config' import { sendEmailTranslated } from './sendEmailTranslated' @@ -66,3 +68,35 @@ export const sendAccountMultiRegistrationEmail = (data: { }, }) } + +export const sendContributionConfirmedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string + contributionAmount: Decimal +}): Promise | null> => { + const rememberLocaleToRestore = i18n.getLocale() + i18n.setLocale(data.language) + const contributionAmount = data.contributionAmount + .toFixed(2) + .replace('.', i18n.__('emails.general.decimalSeparator')) + i18n.setLocale(rememberLocaleToRestore) + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'contributionConfirmed', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + contributionAmount, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 1c1e8968b..8c4f2e00b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -43,6 +43,7 @@ import { findUserByEmail, activationLink, getTimeDurationObject } from './UserRe import { sendAddedContributionMessageEmail, sendAccountActivationEmail, + sendContributionConfirmedEmail, } from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -66,7 +67,6 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi import ContributionMessageArgs from '@arg/ContributionMessageArgs' import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' -import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' import { @@ -584,14 +584,14 @@ export class AdminResolver { await queryRunner.commitTransaction() logger.info('creation commited successfuly.') sendContributionConfirmedEmail({ + firstName: user.firstName, + lastName: user.lastName, + email: user.emailContact.email, + language: user.language, senderFirstName: moderatorUser.firstName, senderLastName: moderatorUser.lastName, - recipientFirstName: user.firstName, - recipientLastName: user.lastName, - recipientEmail: user.emailContact.email, contributionMemo: contribution.memo, contributionAmount: contribution.amount, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) } catch (e) { await queryRunner.rollbackTransaction() diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 85bac73c8..cca40d630 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -2,8 +2,6 @@ "emails": { "addedContributionMessage": { "commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.", - "linkToYourAccount": "Link zu deinem Konto:", - "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag", "toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" }, @@ -22,8 +20,16 @@ "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" }, + "contributionConfirmed": { + "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", + "contributionAmount": "Betrag: {contributionAmount} GDD", + "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" + }, "general": { + "decimalSeparator": ",", "helloName": "Hallo {firstName} {lastName},", + "linkToYourAccount": "Link zu deinem Konto:", + "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" } diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 5207696da..09cf61a9e 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -2,8 +2,6 @@ "emails": { "addedContributionMessage": { "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", - "linkToYourAccount": "Link to your account:", - "pleaseDoNotReply": "Please do not reply to this email!", "subject": "Gradido: Message about your common good contribution", "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!" }, @@ -22,8 +20,16 @@ "onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "subject": "Gradido: Try To Register Again With Your Email" }, + "contributionConfirmed": { + "commonGoodContributionConfirmed": "your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", + "contributionAmount": "Amount: {contributionAmount} GDD", + "subject": "Gradido: Your common good contribution was confirmed" + }, "general": { + "decimalSeparator": ".", "helloName": "Hello {firstName} {lastName}", + "linkToYourAccount": "Link to your account:", + "pleaseDoNotReply": "Please do not reply to this email!", "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" } From 727e5ca1f68d85f6f3d2a962fd9bed14bf1a6e99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 14:45:33 +0100 Subject: [PATCH 22/66] Test 'sendContributionConfirmedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 96 ++++++++++++++++++- .../graphql/resolver/AdminResolver.test.ts | 41 ++++---- 2 files changed, 109 insertions(+), 28 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 38e901828..1d562ebe9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -1,16 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import Decimal from 'decimal.js-light' +import { testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' import CONFIG from '@/config' import { sendAddedContributionMessageEmail, sendAccountActivationEmail, sendAccountMultiRegistrationEmail, + sendContributionConfirmedEmail, } 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' +let con: any +let testEnv: any + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + con = testEnv.con + // await cleanDB() +}) + +afterAll(async () => { + // await cleanDB() + await con.close() +}) jest.mock('./sendEmailTranslated', () => { const originalModule = jest.requireActual('./sendEmailTranslated') @@ -243,4 +257,76 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendContributionConfirmedEmail', () => { + beforeAll(async () => { + result = await sendContributionConfirmedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + contributionAmount: new Decimal(23.54), + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'contributionConfirmed', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + contributionAmount: '23.54', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + 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: Your common good contribution was confirmed', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS CONFIRMED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your common good contribution was confirmed', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your common good contribution was confirmed', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.', + ) + expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') + expect(result.originalMessage.html).toContain( + 'Link to your account: http://localhost/overview', + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 2ff7b0437..d5b4cade5 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -36,13 +36,14 @@ import { } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { User } from '@entity/User' -/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ -import { sendAccountActivationEmail } from '@/emails/sendEmailVariants' +import { + // sendAccountActivationEmail, + sendContributionConfirmedEmail, +} from '@/emails/sendEmailVariants' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' import { Transaction as DbTransaction } from '@entity/Transaction' import { ContributionLink as DbContributionLink } from '@entity/ContributionLink' -import { sendContributionConfirmedEmail } from '@/mailer/sendContributionConfirmedEmail' import { EventProtocol } from '@entity/EventProtocol' import { EventProtocolType } from '@/event/EventProtocolType' @@ -53,15 +54,10 @@ jest.mock('@/emails/sendEmailVariants', () => { __esModule: true, ...originalModule, // TODO: test the call of … - sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), - } -}) - -// mock account activation email to avoid console spam -jest.mock('@/mailer/sendContributionConfirmedEmail', () => { - return { - __esModule: true, - sendContributionConfirmedEmail: jest.fn(), + // sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)), + sendContributionConfirmedEmail: jest.fn((a) => + originalModule.sendContributionConfirmedEmail(a), + ), } }) @@ -1718,17 +1714,16 @@ describe('AdminResolver', () => { }) it('calls sendContributionConfirmedEmail', async () => { - expect(sendContributionConfirmedEmail).toBeCalledWith( - expect.objectContaining({ - contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!', - overviewURL: 'http://localhost/overview', - recipientEmail: 'bibi@bloxberg.de', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - senderFirstName: 'Peter', - senderLastName: 'Lustig', - }), - ) + expect(sendContributionConfirmedEmail).toBeCalledWith({ + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + language: 'de', + senderFirstName: 'Peter', + senderLastName: 'Lustig', + contributionMemo: 'Herzlich Willkommen bei Gradido liebe Bibi!', + contributionAmount: expect.decimalEqual(450), + }) }) it('stores the send confirmation email event in the database', async () => { From dd33254acbc59f579e47eb8fe9967036857dd682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 14:56:09 +0100 Subject: [PATCH 23/66] Remove old untranslated email 'sendContributionConfirmedEmail' --- .../sendContributionConfirmedEmail.test.ts | 39 ------------------- .../mailer/sendContributionConfirmedEmail.ts | 26 ------------- .../src/mailer/text/contributionConfirmed.ts | 30 -------------- 3 files changed, 95 deletions(-) delete mode 100644 backend/src/mailer/sendContributionConfirmedEmail.test.ts delete mode 100644 backend/src/mailer/sendContributionConfirmedEmail.ts delete mode 100644 backend/src/mailer/text/contributionConfirmed.ts diff --git a/backend/src/mailer/sendContributionConfirmedEmail.test.ts b/backend/src/mailer/sendContributionConfirmedEmail.test.ts deleted file mode 100644 index bd89afa69..000000000 --- a/backend/src/mailer/sendContributionConfirmedEmail.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import Decimal from 'decimal.js-light' -import { sendContributionConfirmedEmail } from './sendContributionConfirmedEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendContributionConfirmedEmail', () => { - beforeEach(async () => { - await sendContributionConfirmedEmail({ - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', - contributionAmount: new Decimal(200.0), - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: 'Bibi Bloxberg ', - subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining( - '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/sendContributionConfirmedEmail.ts b/backend/src/mailer/sendContributionConfirmedEmail.ts deleted file mode 100644 index 439d240eb..000000000 --- a/backend/src/mailer/sendContributionConfirmedEmail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { contributionConfirmed } from './text/contributionConfirmed' - -export const sendContributionConfirmedEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>, - subject=${contributionConfirmed.de.subject}, - text=${contributionConfirmed.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`, - subject: contributionConfirmed.de.subject, - text: contributionConfirmed.de.text(data), - }) -} diff --git a/backend/src/mailer/text/contributionConfirmed.ts b/backend/src/mailer/text/contributionConfirmed.ts deleted file mode 100644 index 106c3a4c5..000000000 --- a/backend/src/mailer/text/contributionConfirmed.ts +++ /dev/null @@ -1,30 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const contributionConfirmed = { - de: { - subject: 'Dein Gemeinwohl-Beitrag wurde bestätigt', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -dein 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! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 72213988ac0b91b3d433305273cbbc590ed88d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 16:10:29 +0100 Subject: [PATCH 24/66] Refactor 'sendContributionRejectedEmail' email to HTML and translatable --- .../src/emails/contributionRejected/html.pug | 17 +++++++++++++ .../emails/contributionRejected/subject.pug | 1 + backend/src/emails/sendEmailVariants.ts | 24 +++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 11 ++++----- backend/src/locales/de.json | 5 ++++ backend/src/locales/en.json | 7 +++++- 6 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 backend/src/emails/contributionRejected/html.pug create mode 100644 backend/src/emails/contributionRejected/subject.pug diff --git a/backend/src/emails/contributionRejected/html.pug b/backend/src/emails/contributionRejected/html.pug new file mode 100644 index 000000000..07c014f92 --- /dev/null +++ b/backend/src/emails/contributionRejected/html.pug @@ -0,0 +1,17 @@ +doctype html +html(lang=locale) + head + title= t('emails.contributionRejected.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo }) + p= t('emails.contributionRejected.toSeeContributionsAndMessages') + p= t('emails.general.linkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/contributionRejected/subject.pug b/backend/src/emails/contributionRejected/subject.pug new file mode 100644 index 000000000..cdaae4157 --- /dev/null +++ b/backend/src/emails/contributionRejected/subject.pug @@ -0,0 +1 @@ += t('emails.contributionRejected.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 8d1dcf8de..1779cc297 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -100,3 +100,27 @@ export const sendContributionConfirmedEmail = (data: { }, }) } + +export const sendContributionRejectedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + contributionMemo: string +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'contributionRejected', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + contributionMemo: data.contributionMemo, + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 8c4f2e00b..993fe8c3b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -44,6 +44,7 @@ import { sendAddedContributionMessageEmail, sendAccountActivationEmail, sendContributionConfirmedEmail, + sendContributionRejectedEmail, } from '@/emails/sendEmailVariants' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -67,7 +68,6 @@ import { ContributionMessage as DbContributionMessage } from '@entity/Contributi import ContributionMessageArgs from '@arg/ContributionMessageArgs' import { ContributionMessageType } from '@enum/MessageType' import { ContributionMessage } from '@model/ContributionMessage' -import { sendContributionRejectedEmail } from '@/mailer/sendContributionRejectedEmail' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Event, @@ -489,14 +489,13 @@ export class AdminResolver { event.setEventAdminContributionDelete(eventAdminContributionDelete), ) sendContributionRejectedEmail({ + firstName: user.firstName, + lastName: user.lastName, + email: user.emailContact.email, + language: user.language, senderFirstName: moderator.firstName, senderLastName: moderator.lastName, - recipientEmail: user.emailContact.email, - recipientFirstName: user.firstName, - recipientLastName: user.lastName, contributionMemo: contribution.memo, - contributionAmount: contribution.amount, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) return !!res diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index cca40d630..277728439 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -25,6 +25,11 @@ "contributionAmount": "Betrag: {contributionAmount} GDD", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" }, + "contributionRejected": { + "commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.", + "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", + "toSeeContributionsAndMessages": "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“!" + }, "general": { "decimalSeparator": ",", "helloName": "Hallo {firstName} {lastName},", diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 09cf61a9e..d4292b05f 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -3,7 +3,7 @@ "addedContributionMessage": { "commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.", "subject": "Gradido: Message about your common good contribution", - "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!" + "toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" }, "accountActivation": { "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", @@ -25,6 +25,11 @@ "contributionAmount": "Amount: {contributionAmount} GDD", "subject": "Gradido: Your common good contribution was confirmed" }, + "contributionRejected": { + "commonGoodContributionRejected": "your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", + "subject": "Gradido: Your common good contribution was rejected", + "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" + }, "general": { "decimalSeparator": ".", "helloName": "Hello {firstName} {lastName}", From ae65af9df801b56bdc67b8a95544d2b9e118d2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 16:10:41 +0100 Subject: [PATCH 25/66] Test 'sendContributionRejectedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 75 ++++++++++++++++++- .../graphql/resolver/AdminResolver.test.ts | 3 + 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 1d562ebe9..666fea994 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -9,6 +9,7 @@ import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail, sendContributionConfirmedEmail, + sendContributionRejectedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -99,7 +100,7 @@ describe('sendEmailVariants', () => { 'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.', ) expect(result.originalMessage.html).toContain( - 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My Contributions to the Common Good” tab!', + 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', ) expect(result.originalMessage.html).toContain( 'Link to your account: http://localhost/overview', @@ -329,4 +330,76 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendContributionRejectedEmail', () => { + beforeAll(async () => { + result = await sendContributionRejectedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'contributionRejected', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + contributionMemo: 'My contribution.', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + 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: Your common good contribution was rejected', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS REJECTED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your common good contribution was rejected', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your common good contribution was rejected', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'your public good contribution “My contribution.” was rejected by Bibi Bloxberg.', + ) + expect(result.originalMessage.html).toContain( + 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', + ) + expect(result.originalMessage.html).toContain( + 'Link to your account: http://localhost/overview', + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index d5b4cade5..5c9e3250e 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -39,6 +39,7 @@ import { User } from '@entity/User' import { // sendAccountActivationEmail, sendContributionConfirmedEmail, + // sendContributionRejectedEmail, } from '@/emails/sendEmailVariants' import Decimal from 'decimal.js-light' import { Contribution } from '@entity/Contribution' @@ -58,6 +59,8 @@ jest.mock('@/emails/sendEmailVariants', () => { sendContributionConfirmedEmail: jest.fn((a) => originalModule.sendContributionConfirmedEmail(a), ), + // TODO: test the call of … + // sendContributionRejectedEmail: jest.fn((a) => originalModule.sendContributionRejectedEmail(a)), } }) From 3b030e461270b1e2f48f534b9c7020207191a115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 16:12:48 +0100 Subject: [PATCH 26/66] Remove old untranslated email 'sendContributionRejectedEmail' --- .../sendContributionRejectedEmail.test.ts | 38 ------------------- .../mailer/sendContributionRejectedEmail.ts | 26 ------------- .../src/mailer/text/contributionRejected.ts | 28 -------------- 3 files changed, 92 deletions(-) delete mode 100644 backend/src/mailer/sendContributionRejectedEmail.test.ts delete mode 100644 backend/src/mailer/sendContributionRejectedEmail.ts delete mode 100644 backend/src/mailer/text/contributionRejected.ts diff --git a/backend/src/mailer/sendContributionRejectedEmail.test.ts b/backend/src/mailer/sendContributionRejectedEmail.test.ts deleted file mode 100644 index be41ff15f..000000000 --- a/backend/src/mailer/sendContributionRejectedEmail.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import Decimal from 'decimal.js-light' -import { sendContributionRejectedEmail } from './sendContributionRejectedEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendContributionConfirmedEmail', () => { - beforeEach(async () => { - await sendContributionRejectedEmail({ - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - recipientEmail: 'bibi@bloxberg.de', - contributionMemo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', - contributionAmount: new Decimal(200.0), - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: 'Bibi Bloxberg ', - subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining( - '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/sendContributionRejectedEmail.ts b/backend/src/mailer/sendContributionRejectedEmail.ts deleted file mode 100644 index 9edb5ba2a..000000000 --- a/backend/src/mailer/sendContributionRejectedEmail.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { contributionRejected } from './text/contributionRejected' - -export const sendContributionRejectedEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - recipientEmail: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>, - subject=${contributionRejected.de.subject}, - text=${contributionRejected.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.recipientEmail}>`, - subject: contributionRejected.de.subject, - text: contributionRejected.de.text(data), - }) -} diff --git a/backend/src/mailer/text/contributionRejected.ts b/backend/src/mailer/text/contributionRejected.ts deleted file mode 100644 index ff52c7b5a..000000000 --- a/backend/src/mailer/text/contributionRejected.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const contributionRejected = { - de: { - subject: 'Dein Gemeinwohl-Beitrag wurde abgelehnt', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - contributionMemo: string - contributionAmount: Decimal - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -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! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 44f2e6b06a03a049415f61900bb9b01b6eb7b225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 20:15:55 +0100 Subject: [PATCH 27/66] Move 'getTimeDurationObject' and 'printTimeDuration' into a separate file --- backend/src/graphql/resolver/AdminResolver.ts | 3 ++- .../src/graphql/resolver/UserResolver.test.ts | 3 ++- backend/src/graphql/resolver/UserResolver.ts | 18 +----------------- backend/src/util/time.ts | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 19 deletions(-) create mode 100644 backend/src/util/time.ts diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 993fe8c3b..574377dce 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -39,7 +39,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' -import { findUserByEmail, activationLink, getTimeDurationObject } from './UserResolver' +import { getTimeDurationObject } from '@/util/time' +import { findUserByEmail, activationLink } from './UserResolver' import { sendAddedContributionMessageEmail, sendAccountActivationEmail, diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index dac45c289..2be2361e9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -3,6 +3,7 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { logger, i18n as localization } from '@test/testSetup' +import { printTimeDuration } from '@/util/time' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { @@ -24,7 +25,7 @@ import { sendAccountMultiRegistrationEmail, } from '@/emails/sendEmailVariants' import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' -import { printTimeDuration, activationLink } from './UserResolver' +import { activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ddfa94eab..4ec4af166 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -9,6 +9,7 @@ import { User } from '@model/User' import { User as DbUser } from '@entity/User' import { UserContact as DbUserContact } from '@entity/UserContact' import { communityDbUser } from '@/util/communityUser' +import { getTimeDurationObject, printTimeDuration } from '@/util/time' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { ContributionLink as dbContributionLink } from '@entity/ContributionLink' import { encode } from '@/auth/JWT' @@ -916,20 +917,3 @@ const canResendOptIn = (optIn: LoginEmailOptIn): boolean => { const canEmailResend = (updatedAt: Date): boolean => { return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) } - -export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { - if (time > 60) { - return { - hours: Math.floor(time / 60), - minutes: time % 60, - } - } - return { minutes: time } -} - -export const printTimeDuration = (duration: number): string => { - const time = getTimeDurationObject(duration) - const result = time.minutes > 0 ? `${time.minutes} minutes` : '' - if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') - return result -} diff --git a/backend/src/util/time.ts b/backend/src/util/time.ts new file mode 100644 index 000000000..d429c8d6b --- /dev/null +++ b/backend/src/util/time.ts @@ -0,0 +1,16 @@ +export const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { + if (time > 60) { + return { + hours: Math.floor(time / 60), + minutes: time % 60, + } + } + return { minutes: time } +} + +export const printTimeDuration = (duration: number): string => { + const time = getTimeDurationObject(duration) + const result = time.minutes > 0 ? `${time.minutes} minutes` : '' + if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') + return result +} From a652654be76a49de237e5c35fd8d942c1bd98945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 22:09:36 +0100 Subject: [PATCH 28/66] Refactor 'sendResetPasswordEmail' email to HTML and translatable --- backend/src/emails/accountActivation/html.pug | 2 +- backend/src/emails/resetPassword/html.pug | 20 +++++++++++++++++ backend/src/emails/resetPassword/subject.pug | 1 + backend/src/emails/sendEmailVariants.ts | 22 +++++++++++++++++++ backend/src/graphql/resolver/UserResolver.ts | 9 ++++---- backend/src/locales/de.json | 8 ++++++- backend/src/locales/en.json | 12 +++++++--- 7 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 backend/src/emails/resetPassword/html.pug create mode 100644 backend/src/emails/resetPassword/subject.pug diff --git a/backend/src/emails/accountActivation/html.pug b/backend/src/emails/accountActivation/html.pug index 9c631c960..f283e941e 100644 --- a/backend/src/emails/accountActivation/html.pug +++ b/backend/src/emails/accountActivation/html.pug @@ -11,7 +11,7 @@ html(lang=locale) br a(href=activationLink) #{activationLink} br - span= t('emails.accountActivation.orCopyLink') + span= t('emails.general.orCopyLink') p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) br a(href=resendLink) #{resendLink} diff --git a/backend/src/emails/resetPassword/html.pug b/backend/src/emails/resetPassword/html.pug new file mode 100644 index 000000000..a3ced9a75 --- /dev/null +++ b/backend/src/emails/resetPassword/html.pug @@ -0,0 +1,20 @@ +doctype html +html(lang=locale) + head + title= t('emails.resetPassword.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.resetPassword.youOrSomeoneResetPassword') + p= t('emails.resetPassword.pleaseClickLink') + br + a(href=resetLink) #{resetLink} + br + span= t('emails.general.orCopyLink') + p= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes }) + br + a(href=resendLink) #{resendLink} + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/resetPassword/subject.pug b/backend/src/emails/resetPassword/subject.pug new file mode 100644 index 000000000..3d2b1f00f --- /dev/null +++ b/backend/src/emails/resetPassword/subject.pug @@ -0,0 +1 @@ += t('emails.resetPassword.subject') \ No newline at end of file diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 1779cc297..e8f208eb0 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -124,3 +124,25 @@ export const sendContributionRejectedEmail = (data: { }, }) } + +export const sendResetPasswordEmail = (data: { + firstName: string + lastName: string + email: string + language: string + resetLink: string + timeDurationObject: Record +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'resetPassword', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + resetLink: data.resetLink, + timeDurationObject: data.timeDurationObject, + resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD, + }, + }) +} diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 4ec4af166..6bdddb7b7 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -18,10 +18,10 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { OptInType } from '@enum/OptInType' -import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail, + sendResetPasswordEmail, } from '@/emails/sendEmailVariants' import { klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' @@ -574,12 +574,13 @@ export class UserResolver { // optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) logger.info(`optInCode for ${email}=${dbUserContact}`) // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendResetPasswordEmailMailer({ - link: activationLink(dbUserContact.emailVerificationCode), + const emailSent = await sendResetPasswordEmail({ firstName: user.firstName, lastName: user.lastName, email, - duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), + language: user.language, + resetLink: activationLink(dbUserContact.emailVerificationCode), + timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), }) /* uncomment this, when you need the activation link on the console */ diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 277728439..fd9b74662 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -9,7 +9,6 @@ "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:", - "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "subject": "Gradido: E-Mail Überprüfung" }, "accountMultiRegistration": { @@ -30,10 +29,17 @@ "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", "toSeeContributionsAndMessages": "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“!" }, + "resetPassword": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", + "subject": "Gradido: Passwort zurücksetzen", + "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." + }, "general": { "decimalSeparator": ",", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", + "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index d4292b05f..c688102c6 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -9,7 +9,6 @@ "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", "emailRegistered": "Your email address has just been registered with Gradido.", "pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:", - "orCopyLink": "or copy the link above into your browser window.", "subject": "Gradido: Email Verification" }, "accountMultiRegistration": { @@ -21,19 +20,26 @@ "subject": "Gradido: Try To Register Again With Your Email" }, "contributionConfirmed": { - "commonGoodContributionConfirmed": "your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", + "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "contributionAmount": "Amount: {contributionAmount} GDD", "subject": "Gradido: Your common good contribution was confirmed" }, "contributionRejected": { - "commonGoodContributionRejected": "your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", + "commonGoodContributionRejected": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", "subject": "Gradido: Your common good contribution was rejected", "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" }, + "resetPassword": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "pleaseClickLink": "If it was you, please click on the link:", + "subject": "Gradido: Reset password", + "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." + }, "general": { "decimalSeparator": ".", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", + "orCopyLink": "or copy the link above into your browser window.", "pleaseDoNotReply": "Please do not reply to this email!", "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" From 2d634b4111cc789e80cfd0bf1546887c03b1b9f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 22:10:07 +0100 Subject: [PATCH 29/66] Test 'sendResetPasswordEmail' --- backend/src/emails/sendEmailVariants.test.ts | 77 ++++++++++++++++++- .../src/graphql/resolver/UserResolver.test.ts | 18 ++--- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 666fea994..dd5bb68e9 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -10,6 +10,7 @@ import { sendAccountMultiRegistrationEmail, sendContributionConfirmedEmail, sendContributionRejectedEmail, + sendResetPasswordEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -319,7 +320,7 @@ describe('sendEmailVariants', () => { ) expect(result.originalMessage.html).toContain('Hello Peter Lustig') expect(result.originalMessage.html).toContain( - 'your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.', + 'Your public good contribution “My contribution.” has just been confirmed by Bibi Bloxberg and credited to your Gradido account.', ) expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') expect(result.originalMessage.html).toContain( @@ -389,7 +390,7 @@ describe('sendEmailVariants', () => { ) expect(result.originalMessage.html).toContain('Hello Peter Lustig') expect(result.originalMessage.html).toContain( - 'your public good contribution “My contribution.” was rejected by Bibi Bloxberg.', + 'Your public good contribution “My contribution.” was rejected by Bibi Bloxberg.', ) expect(result.originalMessage.html).toContain( 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', @@ -402,4 +403,76 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendResetPasswordEmail', () => { + beforeAll(async () => { + result = await sendResetPasswordEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + resetLink: 'http://localhost/reset-password/3762660021544901417', + timeDurationObject: { hours: 23, minutes: 30 }, + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'resetPassword', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + resetLink: 'http://localhost/reset-password/3762660021544901417', + timeDurationObject: { hours: 23, minutes: 30 }, + 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: Reset password', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: RESET PASSWORD'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('Gradido: Reset password') + expect(result.originalMessage.html).toContain('>Gradido: Reset password') + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'You, or someone else, requested a password reset for this account.', + ) + expect(result.originalMessage.html).toContain('If it was you, please click on the link:') + expect(result.originalMessage.html).toContain( + 'http://localhost/reset-password/3762660021544901417', + ) + expect(result.originalMessage.html).toContain( + 'or copy the link above into your browser window.', + ) + expect(result.originalMessage.html).toContain( + 'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:', + ) + expect(result.originalMessage.html).toContain( + `${CONFIG.EMAIL_LINK_FORGOTPASSWORD}`, + ) + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 2be2361e9..ee88e58cc 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -23,8 +23,8 @@ import CONFIG from '@/config' import { sendAccountActivationEmail, sendAccountMultiRegistrationEmail, + sendResetPasswordEmail, } from '@/emails/sendEmailVariants' -import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' @@ -53,13 +53,7 @@ jest.mock('@/emails/sendEmailVariants', () => { sendAccountMultiRegistrationEmail: jest.fn((a) => originalModule.sendAccountMultiRegistrationEmail(a), ), - } -}) - -jest.mock('@/mailer/sendResetPasswordEmail', () => { - return { - __esModule: true, - sendResetPasswordEmail: jest.fn(), + sendResetPasswordEmail: jest.fn((a) => originalModule.sendResetPasswordEmail(a)), } }) @@ -857,11 +851,15 @@ describe('UserResolver', () => { it('sends reset password email', () => { expect(sendResetPasswordEmail).toBeCalledWith({ - link: activationLink(emailContact.emailVerificationCode), firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de', - duration: expect.any(String), + language: 'de', + resetLink: activationLink(emailContact.emailVerificationCode), + timeDurationObject: expect.objectContaining({ + hours: expect.any(Number), + minutes: expect.any(Number), + }), }) }) From b68f550b6cc8952e5c91d6de2e8cd82fee8fec1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Thu, 24 Nov 2022 22:16:18 +0100 Subject: [PATCH 30/66] Remove old untranslated email 'sendResetPasswordEmail' --- .../src/mailer/sendResetPasswordEmail.test.ts | 32 ------------------- backend/src/mailer/sendResetPasswordEmail.ts | 17 ---------- backend/src/mailer/text/resetPassword.ts | 30 ----------------- 3 files changed, 79 deletions(-) delete mode 100644 backend/src/mailer/sendResetPasswordEmail.test.ts delete mode 100644 backend/src/mailer/sendResetPasswordEmail.ts delete mode 100644 backend/src/mailer/text/resetPassword.ts diff --git a/backend/src/mailer/sendResetPasswordEmail.test.ts b/backend/src/mailer/sendResetPasswordEmail.test.ts deleted file mode 100644 index 94f69cf8b..000000000 --- a/backend/src/mailer/sendResetPasswordEmail.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { sendResetPasswordEmail } from './sendResetPasswordEmail' -import { sendEMail } from './sendEMail' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendResetPasswordEmail', () => { - beforeEach(async () => { - await sendResetPasswordEmail({ - link: 'resetLink', - firstName: 'Peter', - lastName: 'Lustig', - email: 'peter@lustig.de', - duration: '23 hours and 30 minutes', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Peter Lustig `, - subject: 'Gradido: Passwort zurücksetzen', - text: - expect.stringContaining('Hallo Peter Lustig') && - expect.stringContaining('resetLink') && - expect.stringContaining('23 Stunden und 30 Minuten'), - }) - }) -}) diff --git a/backend/src/mailer/sendResetPasswordEmail.ts b/backend/src/mailer/sendResetPasswordEmail.ts deleted file mode 100644 index d9770f940..000000000 --- a/backend/src/mailer/sendResetPasswordEmail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { sendEMail } from './sendEMail' -import { resetPassword } from './text/resetPassword' -import CONFIG from '@/config' - -export const sendResetPasswordEmail = (data: { - link: string - firstName: string - lastName: string - email: string - duration: string -}): Promise => { - return sendEMail({ - to: `${data.firstName} ${data.lastName} <${data.email}>`, - subject: resetPassword.de.subject, - text: resetPassword.de.text({ ...data, resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD }), - }) -} diff --git a/backend/src/mailer/text/resetPassword.ts b/backend/src/mailer/text/resetPassword.ts deleted file mode 100644 index ff660f76e..000000000 --- a/backend/src/mailer/text/resetPassword.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const resetPassword = { - de: { - subject: 'Gradido: Passwort zurücksetzen', - text: (data: { - link: string - firstName: string - lastName: string - email: string - duration: string - resendLink: string - }): string => - `Hallo ${data.firstName} ${data.lastName}, - -Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert. -Wenn du es warst, klicke bitte auf den Link: ${data.link} -oder kopiere den obigen Link in Dein Browserfenster. - -Der Link hat eine Gültigkeit von ${data.duration - .replace('hours', 'Stunden') - .replace('minutes', 'Minuten') - .replace( - ' and ', - ' und ', - )}. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst: -${data.resendLink} - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} From 84613af035019c91a93ca12863783018bece2528 Mon Sep 17 00:00:00 2001 From: mahula Date: Fri, 25 Nov 2022 17:38:25 +0100 Subject: [PATCH 31/66] disable submit button of contribution messages formular until submit request is finished --- .../ContributionMessagesFormular.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue index c9c285eef..5fd9acf49 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue +++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue @@ -38,10 +38,12 @@ export default { form: { text: '', }, + isSubmitting: false, } }, methods: { onSubmit(event) { + this.isSubmitting = true this.$apollo .mutate({ mutation: adminCreateContributionMessage, @@ -55,9 +57,11 @@ export default { this.$emit('update-state', this.contributionId) this.form.text = '' this.toastSuccess(this.$t('message.request')) + this.isSubmitting = false }) .catch((error) => { this.toastError(error.message) + this.isSubmitting = false }) }, onReset(event) { @@ -66,10 +70,7 @@ export default { }, computed: { disabled() { - if (this.form.text !== '') { - return false - } - return true + return this.form.text === '' || this.isSubmitting }, }, } From 06fd7c9b9a424d0b27c0ca0d77402eb34acf7c27 Mon Sep 17 00:00:00 2001 From: mahula Date: Fri, 25 Nov 2022 17:41:04 +0100 Subject: [PATCH 32/66] disable submit button of contribution messages formular until submit request is finished in wallet --- .../ContributionMessagesFormular.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue index c601de4f5..8667f7a0f 100644 --- a/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue +++ b/frontend/src/components/ContributionMessages/ContributionMessagesFormular.vue @@ -38,10 +38,12 @@ export default { form: { text: '', }, + isSubmitting: false, } }, methods: { onSubmit() { + this.isSubmitting = true this.$apollo .mutate({ mutation: createContributionMessage, @@ -55,9 +57,11 @@ export default { this.$emit('update-state', this.contributionId) this.form.text = '' this.toastSuccess(this.$t('message.reply')) + this.isSubmitting = false }) .catch((error) => { this.toastError(error.message) + this.isSubmitting = false }) }, onReset() { @@ -66,10 +70,7 @@ export default { }, computed: { disabled() { - if (this.form.text !== '') { - return false - } - return true + return this.form.text === '' || this.isSubmitting }, }, } From 4b097f6f66ddd35a47fbe76ef750365843377269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 12:59:06 +0100 Subject: [PATCH 33/66] Add line ends to all 'subject.pug' files --- backend/src/emails/accountActivation/subject.pug | 2 +- backend/src/emails/accountMultiRegistration/subject.pug | 2 +- backend/src/emails/addedContributionMessage/subject.pug | 2 +- backend/src/emails/contributionConfirmed/subject.pug | 2 +- backend/src/emails/contributionRejected/subject.pug | 2 +- backend/src/emails/resetPassword/subject.pug | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/src/emails/accountActivation/subject.pug b/backend/src/emails/accountActivation/subject.pug index 378053bbf..81749a38e 100644 --- a/backend/src/emails/accountActivation/subject.pug +++ b/backend/src/emails/accountActivation/subject.pug @@ -1 +1 @@ -= t('emails.accountActivation.subject') \ No newline at end of file += t('emails.accountActivation.subject') diff --git a/backend/src/emails/accountMultiRegistration/subject.pug b/backend/src/emails/accountMultiRegistration/subject.pug index 322f07c78..fb130f0e4 100644 --- a/backend/src/emails/accountMultiRegistration/subject.pug +++ b/backend/src/emails/accountMultiRegistration/subject.pug @@ -1 +1 @@ -= t('emails.accountMultiRegistration.subject') \ No newline at end of file += t('emails.accountMultiRegistration.subject') diff --git a/backend/src/emails/addedContributionMessage/subject.pug b/backend/src/emails/addedContributionMessage/subject.pug index 8620725f8..4ac85fa23 100644 --- a/backend/src/emails/addedContributionMessage/subject.pug +++ b/backend/src/emails/addedContributionMessage/subject.pug @@ -1 +1 @@ -= t('emails.addedContributionMessage.subject') \ No newline at end of file += t('emails.addedContributionMessage.subject') diff --git a/backend/src/emails/contributionConfirmed/subject.pug b/backend/src/emails/contributionConfirmed/subject.pug index 7e74a77c6..c5bd41421 100644 --- a/backend/src/emails/contributionConfirmed/subject.pug +++ b/backend/src/emails/contributionConfirmed/subject.pug @@ -1 +1 @@ -= t('emails.contributionConfirmed.subject') \ No newline at end of file += t('emails.contributionConfirmed.subject') diff --git a/backend/src/emails/contributionRejected/subject.pug b/backend/src/emails/contributionRejected/subject.pug index cdaae4157..40a7622b8 100644 --- a/backend/src/emails/contributionRejected/subject.pug +++ b/backend/src/emails/contributionRejected/subject.pug @@ -1 +1 @@ -= t('emails.contributionRejected.subject') \ No newline at end of file += t('emails.contributionRejected.subject') diff --git a/backend/src/emails/resetPassword/subject.pug b/backend/src/emails/resetPassword/subject.pug index 3d2b1f00f..21f277316 100644 --- a/backend/src/emails/resetPassword/subject.pug +++ b/backend/src/emails/resetPassword/subject.pug @@ -1 +1 @@ -= t('emails.resetPassword.subject') \ No newline at end of file += t('emails.resetPassword.subject') From 224613f6d44b53090b6e2de464568eba9c7548e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 15:42:44 +0100 Subject: [PATCH 34/66] Refactor 'sendTransactionLinkRedeemedEmail' email to HTML and translatable --- .../src/emails/contributionConfirmed/html.pug | 2 +- backend/src/emails/sendEmailVariants.ts | 38 +++++++++++++++---- .../emails/transactionLinkRedeemed/html.pug | 19 ++++++++++ .../transactionLinkRedeemed/subject.pug | 1 + .../graphql/resolver/TransactionResolver.ts | 14 +++---- backend/src/locales/de.json | 26 ++++++++----- backend/src/locales/en.json | 26 ++++++++----- backend/src/util/utilities.ts | 9 +++++ 8 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 backend/src/emails/transactionLinkRedeemed/html.pug create mode 100644 backend/src/emails/transactionLinkRedeemed/subject.pug diff --git a/backend/src/emails/contributionConfirmed/html.pug b/backend/src/emails/contributionConfirmed/html.pug index e60e6c700..32626b147 100644 --- a/backend/src/emails/contributionConfirmed/html.pug +++ b/backend/src/emails/contributionConfirmed/html.pug @@ -7,7 +7,7 @@ html(lang=locale) #container.col p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo }) - p= t('emails.contributionConfirmed.contributionAmount', { contributionAmount }) + p= t('emails.general.amountGDD', { amountGDD: contributionAmount }) p= t('emails.general.linkToYourAccount') span= " " a(href=overviewURL) #{overviewURL} diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index e8f208eb0..953ac4af5 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -1,6 +1,6 @@ -import i18n from 'i18n' import Decimal from 'decimal.js-light' import CONFIG from '@/config' +import { decimalSeparatorByLanguage } from '@/util/utilities' import { sendEmailTranslated } from './sendEmailTranslated' export const sendAddedContributionMessageEmail = (data: { @@ -79,12 +79,6 @@ export const sendContributionConfirmedEmail = (data: { contributionMemo: string contributionAmount: Decimal }): Promise | null> => { - const rememberLocaleToRestore = i18n.getLocale() - i18n.setLocale(data.language) - const contributionAmount = data.contributionAmount - .toFixed(2) - .replace('.', i18n.__('emails.general.decimalSeparator')) - i18n.setLocale(rememberLocaleToRestore) return sendEmailTranslated({ receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, template: 'contributionConfirmed', @@ -95,7 +89,7 @@ export const sendContributionConfirmedEmail = (data: { senderFirstName: data.senderFirstName, senderLastName: data.senderLastName, contributionMemo: data.contributionMemo, - contributionAmount, + contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language), overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }, }) @@ -146,3 +140,31 @@ export const sendResetPasswordEmail = (data: { }, }) } + +export const sendTransactionLinkRedeemedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + senderEmail: string + transactionMemo: string + transactionAmount: Decimal +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'transactionLinkRedeemed', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + senderEmail: data.senderEmail, + transactionMemo: data.transactionMemo, + transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/emails/transactionLinkRedeemed/html.pug b/backend/src/emails/transactionLinkRedeemed/html.pug new file mode 100644 index 000000000..f15a278c9 --- /dev/null +++ b/backend/src/emails/transactionLinkRedeemed/html.pug @@ -0,0 +1,19 @@ +doctype html +html(lang=locale) + head + title= t('emails.transactionLinkRedeemed.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail }) + p= t('emails.general.amountGDD', { amountGDD: transactionAmount }) + br + span= t('emails.transactionLinkRedeemed.memo', { transactionMemo }) + p= t('emails.transactionLinkRedeemed.detailsYouFindOnLinkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/transactionLinkRedeemed/subject.pug b/backend/src/emails/transactionLinkRedeemed/subject.pug new file mode 100644 index 000000000..6f4f74f04 --- /dev/null +++ b/backend/src/emails/transactionLinkRedeemed/subject.pug @@ -0,0 +1 @@ += t('emails.transactionLinkRedeemed.subject') diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f0fb2f452..d83d99132 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -36,7 +36,7 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByEmail } from './UserResolver' -import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' +import { sendTransactionLinkRedeemedEmail } from '@/emails/sendEmailVariants' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Decay } from '../model/Decay' @@ -182,15 +182,15 @@ export const executeTransaction = async ( }) if (transactionLink) { await sendTransactionLinkRedeemedEmail({ + firstName: sender.firstName, + lastName: sender.lastName, + email: sender.emailContact.email, + language: sender.language, senderFirstName: recipient.firstName, senderLastName: recipient.lastName, - recipientFirstName: sender.firstName, - recipientLastName: sender.lastName, - email: sender.emailContact.email, senderEmail: recipient.emailContact.email, - amount, - memo, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + transactionAmount: amount, + transactionMemo: memo, }) } logger.info(`finished executeTransaction successfully`) diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index fd9b74662..de1e657fe 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -21,7 +21,6 @@ }, "contributionConfirmed": { "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", - "contributionAmount": "Betrag: {contributionAmount} GDD", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" }, "contributionRejected": { @@ -29,20 +28,29 @@ "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", "toSeeContributionsAndMessages": "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“!" }, - "resetPassword": { - "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", - "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", - "subject": "Gradido: Passwort zurücksetzen", - "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." - }, "general": { - "decimalSeparator": ",", + "amountGDD": "Betrag: {amountGDD} GDD", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "pleaseDoNotReply": "Bitte antworte nicht auf diese E-Mail!", "sincerelyYours": "Liebe Grüße", "yourGradidoTeam": "dein Gradido-Team" + }, + "resetPassword": { + "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:", + "pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:", + "subject": "Gradido: Passwort zurücksetzen", + "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." + }, + "transactionLinkRedeemed": { + "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", + "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.", + "memo": "Memo: {transactionMemo}", + "subject": "Gradido: Dein Gradido-Link wurde eingelöst" } + }, + "general": { + "decimalSeparator": "," } -} \ No newline at end of file +} diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index c688102c6..34cf2512f 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -21,7 +21,6 @@ }, "contributionConfirmed": { "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", - "contributionAmount": "Amount: {contributionAmount} GDD", "subject": "Gradido: Your common good contribution was confirmed" }, "contributionRejected": { @@ -29,20 +28,29 @@ "subject": "Gradido: Your common good contribution was rejected", "toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!" }, - "resetPassword": { - "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", - "pleaseClickLink": "If it was you, please click on the link:", - "subject": "Gradido: Reset password", - "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." - }, "general": { - "decimalSeparator": ".", + "amountGDD": "Amount: {amountGDD} GDD", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", "orCopyLink": "or copy the link above into your browser window.", "pleaseDoNotReply": "Please do not reply to this email!", "sincerelyYours": "Kind regards,", "yourGradidoTeam": "your Gradido team" + }, + "resetPassword": { + "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:", + "pleaseClickLink": "If it was you, please click on the link:", + "subject": "Gradido: Reset password", + "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." + }, + "transactionLinkRedeemed": { + "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", + "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.", + "memo": "Memo: {transactionMemo}", + "subject": "Gradido: Your Gradido link has been redeemed" } + }, + "general": { + "decimalSeparator": "." } -} \ No newline at end of file +} diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 65214ebb5..f24def721 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,4 +1,5 @@ import Decimal from 'decimal.js-light' +import i18n from 'i18n' export const objectValuesToArray = (obj: { [x: string]: string }): Array => { return Object.keys(obj).map(function (key) { @@ -15,3 +16,11 @@ export const decimalAddition = (a: Decimal, b: Decimal): Decimal => { export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => { return a.minus(b.toString()) } + +export const decimalSeparatorByLanguage = (a: Decimal, language: string): string => { + const rememberLocaleToRestore = i18n.getLocale() + i18n.setLocale(language) + const result = a.toFixed(2).replace('.', i18n.__('general.decimalSeparator')) + i18n.setLocale(rememberLocaleToRestore) + return result +} From 699daa8eec5c1851ab07ef7b6e890b370f88be6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 15:43:44 +0100 Subject: [PATCH 35/66] Test 'sendTransactionLinkRedeemedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 82 +++++++++++++++++++- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index dd5bb68e9..858b6426b 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -11,6 +11,7 @@ import { sendContributionConfirmedEmail, sendContributionRejectedEmail, sendResetPasswordEmail, + sendTransactionLinkRedeemedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -104,7 +105,7 @@ describe('sendEmailVariants', () => { 'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', ) expect(result.originalMessage.html).toContain( - 'Link to your account: http://localhost/overview', + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, ) expect(result.originalMessage.html).toContain('Please do not reply to this email!') expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') @@ -324,7 +325,7 @@ describe('sendEmailVariants', () => { ) expect(result.originalMessage.html).toContain('Amount: 23.54 GDD') expect(result.originalMessage.html).toContain( - 'Link to your account: http://localhost/overview', + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, ) expect(result.originalMessage.html).toContain('Please do not reply to this email!') expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') @@ -396,7 +397,7 @@ describe('sendEmailVariants', () => { 'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!', ) expect(result.originalMessage.html).toContain( - 'Link to your account: http://localhost/overview', + `Link to your account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, ) expect(result.originalMessage.html).toContain('Please do not reply to this email!') expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') @@ -475,4 +476,79 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendTransactionLinkRedeemedEmail', () => { + beforeAll(async () => { + result = await sendTransactionLinkRedeemedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionMemo: 'You deserve it! 🙏🏼', + transactionAmount: new Decimal(17.65), + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'transactionLinkRedeemed', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionMemo: 'You deserve it! 🙏🏼', + transactionAmount: '17.65', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + 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: Your Gradido link has been redeemed', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOUR GRADIDO LINK HAS BEEN REDEEMED'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: Your Gradido link has been redeemed', + ) + expect(result.originalMessage.html).toContain( + '>Gradido: Your Gradido link has been redeemed', + ) + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.', + ) + expect(result.originalMessage.html).toContain('Amount: 17.65 GDD') + expect(result.originalMessage.html).toContain('Memo: You deserve it! 🙏🏼') + expect(result.originalMessage.html).toContain( + `You can find transaction details in your Gradido account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) From 2b65c6b2613bab1d4576b07fb73e8c0cdac8c653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Sat, 26 Nov 2022 15:45:41 +0100 Subject: [PATCH 36/66] Remove old untranslated email 'sendTransactionLinkRedeemedEmail' --- .../sendTransactionLinkRedeemed.test.ts | 44 ------------------- .../src/mailer/sendTransactionLinkRedeemed.ts | 28 ------------ .../mailer/text/transactionLinkRedeemed.ts | 33 -------------- 3 files changed, 105 deletions(-) delete mode 100644 backend/src/mailer/sendTransactionLinkRedeemed.test.ts delete mode 100644 backend/src/mailer/sendTransactionLinkRedeemed.ts delete mode 100644 backend/src/mailer/text/transactionLinkRedeemed.ts diff --git a/backend/src/mailer/sendTransactionLinkRedeemed.test.ts b/backend/src/mailer/sendTransactionLinkRedeemed.test.ts deleted file mode 100644 index b56ff40a1..000000000 --- a/backend/src/mailer/sendTransactionLinkRedeemed.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { sendEMail } from './sendEMail' -import Decimal from 'decimal.js-light' -import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendTransactionLinkRedeemedEmail', () => { - beforeEach(async () => { - await sendTransactionLinkRedeemedEmail({ - email: 'bibi@bloxberg.de', - senderFirstName: 'Peter', - senderLastName: 'Lustig', - recipientFirstName: 'Bibi', - recipientLastName: 'Bloxberg', - senderEmail: 'peter@lustig.de', - amount: new Decimal(42.0), - memo: 'Vielen Dank dass Du dabei bist', - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Bibi Bloxberg `, - subject: 'Gradido-Link wurde eingelöst', - text: - expect.stringContaining('Hallo Bibi Bloxberg') && - expect.stringContaining( - 'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.', - ) && - expect.stringContaining('Betrag: 42,00 GDD,') && - expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') && - expect.stringContaining( - 'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview', - ) && - expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'), - }) - }) -}) diff --git a/backend/src/mailer/sendTransactionLinkRedeemed.ts b/backend/src/mailer/sendTransactionLinkRedeemed.ts deleted file mode 100644 index a78f3b3c9..000000000 --- a/backend/src/mailer/sendTransactionLinkRedeemed.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { transactionLinkRedeemed } from './text/transactionLinkRedeemed' - -export const sendTransactionLinkRedeemedEmail = (data: { - email: string - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - senderEmail: string - amount: Decimal - memo: string - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName}, - <${data.email}>, - subject=${transactionLinkRedeemed.de.subject}, - text=${transactionLinkRedeemed.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, - subject: transactionLinkRedeemed.de.subject, - text: transactionLinkRedeemed.de.text(data), - }) -} diff --git a/backend/src/mailer/text/transactionLinkRedeemed.ts b/backend/src/mailer/text/transactionLinkRedeemed.ts deleted file mode 100644 index a63e5d275..000000000 --- a/backend/src/mailer/text/transactionLinkRedeemed.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const transactionLinkRedeemed = { - de: { - subject: 'Gradido-Link wurde eingelöst', - text: (data: { - email: string - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - senderEmail: string - amount: Decimal - memo: string - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -${data.senderFirstName} ${data.senderLastName} (${ - data.senderEmail - }) hat soeben deinen Link eingelöst. - -Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD, -Memo: ${data.memo} - -Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 0cccdb56de511b9d16ffc0c3f44ad43e03d3071a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 09:38:19 +0100 Subject: [PATCH 37/66] Test that 'sendEmailTranslated' on 'CONFIG.EMAIL_TEST_MODUS = true' replaces the receiver with by the fake test mail accaount --- .../src/emails/sendEmailTranslated.test.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index 28327f779..72c4c508c 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -107,4 +107,41 @@ describe('sendEmailTranslated', () => { expect(i18n.__).toBeCalled() }) }) + + describe('with email EMAIL_TEST_MODUS true', () => { + beforeEach(async () => { + jest.clearAllMocks() + CONFIG.EMAIL = true + CONFIG.EMAIL_TEST_MODUS = true + result = await sendEmailTranslated({ + receiver: { + to: 'receiver@mail.org', + cc: 'support@gradido.net', + }, + template: 'accountMultiRegistration', + locals: { + locale: 'en', + }, + }) + }) + + it('call of "sendEmailTranslated" with faked "to"', () => { + expect(result).toMatchObject({ + envelope: { + from: CONFIG.EMAIL_SENDER, + to: [CONFIG.EMAIL_TEST_RECEIVER, 'support@gradido.net'], + }, + message: expect.any(String), + originalMessage: expect.objectContaining({ + to: CONFIG.EMAIL_TEST_RECEIVER, + cc: 'support@gradido.net', + from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + 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'), + }), + }) + }) + }) }) From 3903e29e160d48e1adb02dc1cee45000e8d1b346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 10:54:10 +0100 Subject: [PATCH 38/66] Refactor 'sendTransactionReceivedEmail' email to HTML and translatable --- backend/src/emails/sendEmailVariants.ts | 26 +++++++++++++++++++ .../emails/transactionLinkRedeemed/html.pug | 2 +- .../src/emails/transactionReceived/html.pug | 16 ++++++++++++ .../emails/transactionReceived/subject.pug | 1 + .../graphql/resolver/TransactionResolver.ts | 20 +++++++------- backend/src/locales/de.json | 6 ++++- backend/src/locales/en.json | 6 ++++- 7 files changed, 63 insertions(+), 14 deletions(-) create mode 100644 backend/src/emails/transactionReceived/html.pug create mode 100644 backend/src/emails/transactionReceived/subject.pug diff --git a/backend/src/emails/sendEmailVariants.ts b/backend/src/emails/sendEmailVariants.ts index 953ac4af5..356f95e39 100644 --- a/backend/src/emails/sendEmailVariants.ts +++ b/backend/src/emails/sendEmailVariants.ts @@ -168,3 +168,29 @@ export const sendTransactionLinkRedeemedEmail = (data: { }, }) } + +export const sendTransactionReceivedEmail = (data: { + firstName: string + lastName: string + email: string + language: string + senderFirstName: string + senderLastName: string + senderEmail: string + transactionAmount: Decimal +}): Promise | null> => { + return sendEmailTranslated({ + receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` }, + template: 'transactionReceived', + locals: { + firstName: data.firstName, + lastName: data.lastName, + locale: data.language, + senderFirstName: data.senderFirstName, + senderLastName: data.senderLastName, + senderEmail: data.senderEmail, + transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language), + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) +} diff --git a/backend/src/emails/transactionLinkRedeemed/html.pug b/backend/src/emails/transactionLinkRedeemed/html.pug index f15a278c9..321d070b4 100644 --- a/backend/src/emails/transactionLinkRedeemed/html.pug +++ b/backend/src/emails/transactionLinkRedeemed/html.pug @@ -10,7 +10,7 @@ html(lang=locale) p= t('emails.general.amountGDD', { amountGDD: transactionAmount }) br span= t('emails.transactionLinkRedeemed.memo', { transactionMemo }) - p= t('emails.transactionLinkRedeemed.detailsYouFindOnLinkToYourAccount') + p= t('emails.general.detailsYouFindOnLinkToYourAccount') span= " " a(href=overviewURL) #{overviewURL} p= t('emails.general.pleaseDoNotReply') diff --git a/backend/src/emails/transactionReceived/html.pug b/backend/src/emails/transactionReceived/html.pug new file mode 100644 index 000000000..eaf57f975 --- /dev/null +++ b/backend/src/emails/transactionReceived/html.pug @@ -0,0 +1,16 @@ +doctype html +html(lang=locale) + head + title= t('emails.transactionReceived.subject') + body + h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject') + #container.col + p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName }) + p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail }) + p= t('emails.general.detailsYouFindOnLinkToYourAccount') + span= " " + a(href=overviewURL) #{overviewURL} + p= t('emails.general.pleaseDoNotReply') + p(style='margin-top: 24px;')= t('emails.general.sincerelyYours') + br + span= t('emails.general.yourGradidoTeam') diff --git a/backend/src/emails/transactionReceived/subject.pug b/backend/src/emails/transactionReceived/subject.pug new file mode 100644 index 000000000..630752b02 --- /dev/null +++ b/backend/src/emails/transactionReceived/subject.pug @@ -0,0 +1 @@ += t('emails.transactionReceived.subject') diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index d83d99132..1a06f46d9 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -2,14 +2,11 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { backendLogger as logger } from '@/server/logger' -import CONFIG from '@/config' import { Context, getUser } from '@/server/context' import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { getCustomRepository, getConnection, In } from '@dbTools/typeorm' -import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail' - import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' @@ -36,7 +33,10 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { findUserByEmail } from './UserResolver' -import { sendTransactionLinkRedeemedEmail } from '@/emails/sendEmailVariants' +import { + sendTransactionLinkRedeemedEmail, + sendTransactionReceivedEmail, +} from '@/emails/sendEmailVariants' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' import { Decay } from '../model/Decay' @@ -168,17 +168,15 @@ export const executeTransaction = async ( await queryRunner.release() } logger.debug(`prepare Email for transaction received...`) - // send notification email - // TODO: translate await sendTransactionReceivedEmail({ + firstName: recipient.firstName, + lastName: recipient.lastName, + email: recipient.emailContact.email, + language: recipient.language, senderFirstName: sender.firstName, senderLastName: sender.lastName, - recipientFirstName: recipient.firstName, - recipientLastName: recipient.lastName, - email: recipient.emailContact.email, senderEmail: sender.emailContact.email, - amount, - overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + transactionAmount: amount, }) if (transactionLink) { await sendTransactionLinkRedeemedEmail({ diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index de1e657fe..27a943ccc 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -30,6 +30,7 @@ }, "general": { "amountGDD": "Betrag: {amountGDD} GDD", + "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", @@ -44,10 +45,13 @@ "youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert." }, "transactionLinkRedeemed": { - "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.", "memo": "Memo: {transactionMemo}", "subject": "Gradido: Dein Gradido-Link wurde eingelöst" + }, + "transactionReceived": { + "haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.", + "subject": "Gradido: Du hast Gradidos erhalten" } }, "general": { diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index 34cf2512f..fb578bc40 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -30,6 +30,7 @@ }, "general": { "amountGDD": "Amount: {amountGDD} GDD", + "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", "orCopyLink": "or copy the link above into your browser window.", @@ -44,10 +45,13 @@ "youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account." }, "transactionLinkRedeemed": { - "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", "hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.", "memo": "Memo: {transactionMemo}", "subject": "Gradido: Your Gradido link has been redeemed" + }, + "transactionReceived": { + "haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).", + "subject": "Gradido: You have received Gradidos" } }, "general": { From 9894b02f5110263c70031f95a555fa6fc78d8d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 10:54:31 +0100 Subject: [PATCH 39/66] Test 'sendTransactionReceivedEmail' --- backend/src/emails/sendEmailVariants.test.ts | 70 ++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index 858b6426b..dd3c979dd 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -12,6 +12,7 @@ import { sendContributionRejectedEmail, sendResetPasswordEmail, sendTransactionLinkRedeemedEmail, + sendTransactionReceivedEmail, } from './sendEmailVariants' import { sendEmailTranslated } from './sendEmailTranslated' @@ -551,4 +552,73 @@ describe('sendEmailVariants', () => { }) }) }) + + describe('sendTransactionReceivedEmail', () => { + beforeAll(async () => { + result = await sendTransactionReceivedEmail({ + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + language: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionAmount: new Decimal(37.4), + }) + }) + + describe('calls "sendEmailTranslated"', () => { + it('with expected parameters', () => { + expect(sendEmailTranslated).toBeCalledWith({ + receiver: { + to: 'Peter Lustig ', + }, + template: 'transactionReceived', + locals: { + firstName: 'Peter', + lastName: 'Lustig', + locale: 'en', + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + senderEmail: 'bibi@bloxberg.de', + transactionAmount: '37.40', + overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, + }, + }) + }) + + 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: You have received Gradidos', + html: expect.any(String), + text: expect.stringContaining('GRADIDO: YOU HAVE RECEIVED GRADIDOS'), + }), + }) + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain('') + expect(result.originalMessage.html).toContain( + 'Gradido: You have received Gradidos', + ) + expect(result.originalMessage.html).toContain('>Gradido: You have received Gradidos') + expect(result.originalMessage.html).toContain('Hello Peter Lustig') + expect(result.originalMessage.html).toContain( + 'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).', + ) + expect(result.originalMessage.html).toContain( + `You can find transaction details in your Gradido account: ${CONFIG.EMAIL_LINK_OVERVIEW}`, + ) + expect(result.originalMessage.html).toContain('Please do not reply to this email!') + expect(result.originalMessage.html).toContain('Kind regards,
your Gradido team') + }) + }) + }) }) From 93e3562318a1bc135070eea2275c492d97b91798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 11:04:34 +0100 Subject: [PATCH 40/66] Remove old untranslated email 'sendTransactionReceivedEmail' --- .../sendTransactionReceivedEmail.test.ts | 38 ------------------- .../mailer/sendTransactionReceivedEmail.ts | 27 ------------- .../src/mailer/text/transactionReceived.ts | 29 -------------- 3 files changed, 94 deletions(-) delete mode 100644 backend/src/mailer/sendTransactionReceivedEmail.test.ts delete mode 100644 backend/src/mailer/sendTransactionReceivedEmail.ts delete mode 100644 backend/src/mailer/text/transactionReceived.ts diff --git a/backend/src/mailer/sendTransactionReceivedEmail.test.ts b/backend/src/mailer/sendTransactionReceivedEmail.test.ts deleted file mode 100644 index ca813c033..000000000 --- a/backend/src/mailer/sendTransactionReceivedEmail.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail' -import { sendEMail } from './sendEMail' -import Decimal from 'decimal.js-light' - -jest.mock('./sendEMail', () => { - return { - __esModule: true, - sendEMail: jest.fn(), - } -}) - -describe('sendTransactionReceivedEmail', () => { - beforeEach(async () => { - await sendTransactionReceivedEmail({ - senderFirstName: 'Bibi', - senderLastName: 'Bloxberg', - recipientFirstName: 'Peter', - recipientLastName: 'Lustig', - email: 'peter@lustig.de', - senderEmail: 'bibi@bloxberg.de', - amount: new Decimal(42.0), - overviewURL: 'http://localhost/overview', - }) - }) - - it('calls sendEMail', () => { - expect(sendEMail).toBeCalledWith({ - to: `Peter Lustig `, - subject: 'Du hast Gradidos erhalten', - text: - expect.stringContaining('Hallo Peter Lustig') && - expect.stringContaining('42,00 GDD') && - expect.stringContaining('Bibi Bloxberg') && - expect.stringContaining('(bibi@bloxberg.de)') && - expect.stringContaining('http://localhost/overview'), - }) - }) -}) diff --git a/backend/src/mailer/sendTransactionReceivedEmail.ts b/backend/src/mailer/sendTransactionReceivedEmail.ts deleted file mode 100644 index 5e981659c..000000000 --- a/backend/src/mailer/sendTransactionReceivedEmail.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import Decimal from 'decimal.js-light' -import { sendEMail } from './sendEMail' -import { transactionReceived } from './text/transactionReceived' - -export const sendTransactionReceivedEmail = (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - email: string - senderEmail: string - amount: Decimal - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName}, - <${data.email}>, - subject=${transactionReceived.de.subject}, - text=${transactionReceived.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, - subject: transactionReceived.de.subject, - text: transactionReceived.de.text(data), - }) -} diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts deleted file mode 100644 index 67758c0e1..000000000 --- a/backend/src/mailer/text/transactionReceived.ts +++ /dev/null @@ -1,29 +0,0 @@ -import Decimal from 'decimal.js-light' - -export const transactionReceived = { - de: { - subject: 'Du hast Gradidos erhalten', - text: (data: { - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - email: string - senderEmail: string - amount: Decimal - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName}, - -du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${ - data.senderLastName - } (${data.senderEmail}) erhalten. - -Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Liebe Grüße -dein Gradido-Team`, - }, -} From 3b47330c925a5577411cc52b4c01d65365274990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 11:05:12 +0100 Subject: [PATCH 41/66] Remove remainders of old untranslated emails --- backend/src/mailer/sendEMail.test.ts | 113 --------------------------- backend/src/mailer/sendEMail.ts | 48 ------------ 2 files changed, 161 deletions(-) delete mode 100644 backend/src/mailer/sendEMail.test.ts delete mode 100644 backend/src/mailer/sendEMail.ts diff --git a/backend/src/mailer/sendEMail.test.ts b/backend/src/mailer/sendEMail.test.ts deleted file mode 100644 index e062b71d8..000000000 --- a/backend/src/mailer/sendEMail.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { sendEMail } from './sendEMail' -import { createTransport } from 'nodemailer' -import CONFIG from '@/config' - -import { logger } from '@test/testSetup' - -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('sendEMail', () => { - let result: boolean - describe('config email is false', () => { - beforeEach(async () => { - jest.clearAllMocks() - result = await sendEMail({ - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - 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 () => { - jest.clearAllMocks() - CONFIG.EMAIL = true - result = await sendEMail({ - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('calls the transporter', () => { - expect(createTransport).toBeCalledWith({ - host: 'EMAIL_SMTP_URL', - port: 1234, - secure: false, - requireTLS: true, - auth: { - user: 'user', - pass: 'pwd', - }, - }) - }) - - it('calls sendMail of transporter', () => { - expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('returns true', () => { - expect(result).toBeTruthy() - }) - }) - - describe('with email EMAIL_TEST_MODUS true', () => { - beforeEach(async () => { - jest.clearAllMocks() - CONFIG.EMAIL = true - CONFIG.EMAIL_TEST_MODUS = true - result = await sendEMail({ - to: 'receiver@mail.org', - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - - it('calls sendMail of transporter with faked to', () => { - expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: CONFIG.EMAIL_TEST_RECEIVER, - cc: 'support@gradido.net', - subject: 'Subject', - text: 'Text text text', - }) - }) - }) -}) diff --git a/backend/src/mailer/sendEMail.ts b/backend/src/mailer/sendEMail.ts deleted file mode 100644 index 00282f232..000000000 --- a/backend/src/mailer/sendEMail.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { backendLogger as logger } from '@/server/logger' -import { createTransport } from 'nodemailer' - -import CONFIG from '@/config' - -export const sendEMail = async (emailDef: { - to: string - cc?: string - subject: string - text: string -}): Promise => { - logger.info( - `send Email: to=${emailDef.to}` + - (emailDef.cc ? `, cc=${emailDef.cc}` : '') + - `, subject=${emailDef.subject}, text=${emailDef.text}`, - ) - - if (!CONFIG.EMAIL) { - logger.info(`Emails are disabled via config...`) - return false - } - if (CONFIG.EMAIL_TEST_MODUS) { - logger.info( - `Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, - ) - emailDef.to = CONFIG.EMAIL_TEST_RECEIVER - } - const transporter = createTransport({ - host: CONFIG.EMAIL_SMTP_URL, - port: Number(CONFIG.EMAIL_SMTP_PORT), - secure: false, // true for 465, false for other ports - requireTLS: true, - auth: { - user: CONFIG.EMAIL_USERNAME, - pass: CONFIG.EMAIL_PASSWORD, - }, - }) - const info = await transporter.sendMail({ - ...emailDef, - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - }) - if (!info.messageId) { - logger.error('error sending notification email, but transaction succeed') - throw new Error('error sending notification email, but transaction succeed') - } - logger.info('send Email successfully.') - return true -} From 4ae96a221396b8e4632839ea93d93833e8378394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Mon, 28 Nov 2022 11:13:04 +0100 Subject: [PATCH 42/66] Translate 'Gradido (nicht antworten)' --- backend/src/emails/sendEmailTranslated.test.ts | 4 ++-- backend/src/emails/sendEmailTranslated.ts | 2 +- backend/src/emails/sendEmailVariants.test.ts | 16 ++++++++-------- backend/src/locales/de.json | 1 + backend/src/locales/en.json | 1 + 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/src/emails/sendEmailTranslated.test.ts b/backend/src/emails/sendEmailTranslated.test.ts index 72c4c508c..eb4b26b92 100644 --- a/backend/src/emails/sendEmailTranslated.test.ts +++ b/backend/src/emails/sendEmailTranslated.test.ts @@ -89,7 +89,7 @@ describe('sendEmailTranslated', () => { originalMessage: expect.objectContaining({ to: 'receiver@mail.org', cc: 'support@gradido.net', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Try To Register Again With Your Email', html: expect.stringContaining('Gradido: Try To Register Again With Your Email'), @@ -135,7 +135,7 @@ describe('sendEmailTranslated', () => { originalMessage: expect.objectContaining({ to: CONFIG.EMAIL_TEST_RECEIVER, cc: 'support@gradido.net', - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + from: `Gradido (do not answer) <${CONFIG.EMAIL_SENDER}>`, attachments: [], subject: 'Gradido: Try To Register Again With Your Email', html: expect.stringContaining('Gradido: Try To Register Again With Your Email'), diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 9468e9f97..3f5d9d9fd 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -53,7 +53,7 @@ export const sendEmailTranslated = async (params: { // TESTING: see 'README.md' const email = new Email({ message: { - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + from: `Gradido (${i18n.__('emails.general.doNotAnswer')}) <${CONFIG.EMAIL_SENDER}>`, }, transport, preview: false, diff --git a/backend/src/emails/sendEmailVariants.test.ts b/backend/src/emails/sendEmailVariants.test.ts index dd3c979dd..262b91be2 100644 --- a/backend/src/emails/sendEmailVariants.test.ts +++ b/backend/src/emails/sendEmailVariants.test.ts @@ -83,7 +83,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Message about your common good contribution', html: expect.any(String), @@ -153,7 +153,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Email Verification', html: expect.any(String), @@ -223,7 +223,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Try To Register Again With Your Email', html: expect.any(String), @@ -305,7 +305,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Your common good contribution was confirmed', html: expect.any(String), @@ -375,7 +375,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Your common good contribution was rejected', html: expect.any(String), @@ -445,7 +445,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Reset password', html: expect.any(String), @@ -523,7 +523,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: Your Gradido link has been redeemed', html: expect.any(String), @@ -596,7 +596,7 @@ describe('sendEmailVariants', () => { message: expect.any(String), originalMessage: expect.objectContaining({ to: 'Peter Lustig ', - from: 'Gradido (nicht antworten) ', + from: 'Gradido (do not answer) ', attachments: [], subject: 'Gradido: You have received Gradidos', html: expect.any(String), diff --git a/backend/src/locales/de.json b/backend/src/locales/de.json index 27a943ccc..8aff6caa4 100644 --- a/backend/src/locales/de.json +++ b/backend/src/locales/de.json @@ -31,6 +31,7 @@ "general": { "amountGDD": "Betrag: {amountGDD} GDD", "detailsYouFindOnLinkToYourAccount": "Details zur Transaktion findest du in deinem Gradido-Konto:", + "doNotAnswer": "nicht antworten", "helloName": "Hallo {firstName} {lastName},", "linkToYourAccount": "Link zu deinem Konto:", "orCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", diff --git a/backend/src/locales/en.json b/backend/src/locales/en.json index fb578bc40..99217840e 100644 --- a/backend/src/locales/en.json +++ b/backend/src/locales/en.json @@ -31,6 +31,7 @@ "general": { "amountGDD": "Amount: {amountGDD} GDD", "detailsYouFindOnLinkToYourAccount": "You can find transaction details in your Gradido account:", + "doNotAnswer": "do not answer", "helloName": "Hello {firstName} {lastName}", "linkToYourAccount": "Link to your account:", "orCopyLink": "or copy the link above into your browser window.", From 403e68480c0cb7efa3f84544028398fb5a774d76 Mon Sep 17 00:00:00 2001 From: mahula Date: Tue, 29 Nov 2022 11:05:26 +0100 Subject: [PATCH 43/66] change variable name to our standard naming --- .../ContributionMessagesFormular.vue | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue index 5fd9acf49..3a5d6e0b8 100644 --- a/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue +++ b/admin/src/components/ContributionMessages/ContributionMessagesFormular.vue @@ -38,12 +38,12 @@ export default { form: { text: '', }, - isSubmitting: false, + loading: false, } }, methods: { onSubmit(event) { - this.isSubmitting = true + this.loading = true this.$apollo .mutate({ mutation: adminCreateContributionMessage, @@ -57,11 +57,11 @@ export default { this.$emit('update-state', this.contributionId) this.form.text = '' this.toastSuccess(this.$t('message.request')) - this.isSubmitting = false + this.loading = false }) .catch((error) => { this.toastError(error.message) - this.isSubmitting = false + this.loading = false }) }, onReset(event) { @@ -70,7 +70,7 @@ export default { }, computed: { disabled() { - return this.form.text === '' || this.isSubmitting + return this.form.text === '' || this.loading }, }, } From 88cae0e528eea80319306e8dcef8305f01140d84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 13:40:08 +0100 Subject: [PATCH 44/66] Move email '*.pug' templates in 'templates' folder --- backend/src/emails/sendEmailTranslated.ts | 2 +- backend/src/emails/{ => templates}/accountActivation/html.pug | 0 .../src/emails/{ => templates}/accountActivation/subject.pug | 0 .../emails/{ => templates}/accountMultiRegistration/html.pug | 0 .../emails/{ => templates}/accountMultiRegistration/subject.pug | 0 .../emails/{ => templates}/addedContributionMessage/html.pug | 0 .../emails/{ => templates}/addedContributionMessage/subject.pug | 0 .../src/emails/{ => templates}/contributionConfirmed/html.pug | 0 .../emails/{ => templates}/contributionConfirmed/subject.pug | 0 .../src/emails/{ => templates}/contributionRejected/html.pug | 0 .../src/emails/{ => templates}/contributionRejected/subject.pug | 0 backend/src/emails/{ => templates}/resetPassword/html.pug | 0 backend/src/emails/{ => templates}/resetPassword/subject.pug | 0 .../src/emails/{ => templates}/transactionLinkRedeemed/html.pug | 0 .../emails/{ => templates}/transactionLinkRedeemed/subject.pug | 0 backend/src/emails/{ => templates}/transactionReceived/html.pug | 0 .../src/emails/{ => templates}/transactionReceived/subject.pug | 0 17 files changed, 1 insertion(+), 1 deletion(-) rename backend/src/emails/{ => templates}/accountActivation/html.pug (100%) rename backend/src/emails/{ => templates}/accountActivation/subject.pug (100%) rename backend/src/emails/{ => templates}/accountMultiRegistration/html.pug (100%) rename backend/src/emails/{ => templates}/accountMultiRegistration/subject.pug (100%) rename backend/src/emails/{ => templates}/addedContributionMessage/html.pug (100%) rename backend/src/emails/{ => templates}/addedContributionMessage/subject.pug (100%) rename backend/src/emails/{ => templates}/contributionConfirmed/html.pug (100%) rename backend/src/emails/{ => templates}/contributionConfirmed/subject.pug (100%) rename backend/src/emails/{ => templates}/contributionRejected/html.pug (100%) rename backend/src/emails/{ => templates}/contributionRejected/subject.pug (100%) rename backend/src/emails/{ => templates}/resetPassword/html.pug (100%) rename backend/src/emails/{ => templates}/resetPassword/subject.pug (100%) rename backend/src/emails/{ => templates}/transactionLinkRedeemed/html.pug (100%) rename backend/src/emails/{ => templates}/transactionLinkRedeemed/subject.pug (100%) rename backend/src/emails/{ => templates}/transactionReceived/html.pug (100%) rename backend/src/emails/{ => templates}/transactionReceived/subject.pug (100%) diff --git a/backend/src/emails/sendEmailTranslated.ts b/backend/src/emails/sendEmailTranslated.ts index 3f5d9d9fd..69008c00e 100644 --- a/backend/src/emails/sendEmailTranslated.ts +++ b/backend/src/emails/sendEmailTranslated.ts @@ -63,7 +63,7 @@ export const sendEmailTranslated = async (params: { // 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), + template: path.join(__dirname, 'templates', 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' }) diff --git a/backend/src/emails/accountActivation/html.pug b/backend/src/emails/templates/accountActivation/html.pug similarity index 100% rename from backend/src/emails/accountActivation/html.pug rename to backend/src/emails/templates/accountActivation/html.pug diff --git a/backend/src/emails/accountActivation/subject.pug b/backend/src/emails/templates/accountActivation/subject.pug similarity index 100% rename from backend/src/emails/accountActivation/subject.pug rename to backend/src/emails/templates/accountActivation/subject.pug diff --git a/backend/src/emails/accountMultiRegistration/html.pug b/backend/src/emails/templates/accountMultiRegistration/html.pug similarity index 100% rename from backend/src/emails/accountMultiRegistration/html.pug rename to backend/src/emails/templates/accountMultiRegistration/html.pug diff --git a/backend/src/emails/accountMultiRegistration/subject.pug b/backend/src/emails/templates/accountMultiRegistration/subject.pug similarity index 100% rename from backend/src/emails/accountMultiRegistration/subject.pug rename to backend/src/emails/templates/accountMultiRegistration/subject.pug diff --git a/backend/src/emails/addedContributionMessage/html.pug b/backend/src/emails/templates/addedContributionMessage/html.pug similarity index 100% rename from backend/src/emails/addedContributionMessage/html.pug rename to backend/src/emails/templates/addedContributionMessage/html.pug diff --git a/backend/src/emails/addedContributionMessage/subject.pug b/backend/src/emails/templates/addedContributionMessage/subject.pug similarity index 100% rename from backend/src/emails/addedContributionMessage/subject.pug rename to backend/src/emails/templates/addedContributionMessage/subject.pug diff --git a/backend/src/emails/contributionConfirmed/html.pug b/backend/src/emails/templates/contributionConfirmed/html.pug similarity index 100% rename from backend/src/emails/contributionConfirmed/html.pug rename to backend/src/emails/templates/contributionConfirmed/html.pug diff --git a/backend/src/emails/contributionConfirmed/subject.pug b/backend/src/emails/templates/contributionConfirmed/subject.pug similarity index 100% rename from backend/src/emails/contributionConfirmed/subject.pug rename to backend/src/emails/templates/contributionConfirmed/subject.pug diff --git a/backend/src/emails/contributionRejected/html.pug b/backend/src/emails/templates/contributionRejected/html.pug similarity index 100% rename from backend/src/emails/contributionRejected/html.pug rename to backend/src/emails/templates/contributionRejected/html.pug diff --git a/backend/src/emails/contributionRejected/subject.pug b/backend/src/emails/templates/contributionRejected/subject.pug similarity index 100% rename from backend/src/emails/contributionRejected/subject.pug rename to backend/src/emails/templates/contributionRejected/subject.pug diff --git a/backend/src/emails/resetPassword/html.pug b/backend/src/emails/templates/resetPassword/html.pug similarity index 100% rename from backend/src/emails/resetPassword/html.pug rename to backend/src/emails/templates/resetPassword/html.pug diff --git a/backend/src/emails/resetPassword/subject.pug b/backend/src/emails/templates/resetPassword/subject.pug similarity index 100% rename from backend/src/emails/resetPassword/subject.pug rename to backend/src/emails/templates/resetPassword/subject.pug diff --git a/backend/src/emails/transactionLinkRedeemed/html.pug b/backend/src/emails/templates/transactionLinkRedeemed/html.pug similarity index 100% rename from backend/src/emails/transactionLinkRedeemed/html.pug rename to backend/src/emails/templates/transactionLinkRedeemed/html.pug diff --git a/backend/src/emails/transactionLinkRedeemed/subject.pug b/backend/src/emails/templates/transactionLinkRedeemed/subject.pug similarity index 100% rename from backend/src/emails/transactionLinkRedeemed/subject.pug rename to backend/src/emails/templates/transactionLinkRedeemed/subject.pug diff --git a/backend/src/emails/transactionReceived/html.pug b/backend/src/emails/templates/transactionReceived/html.pug similarity index 100% rename from backend/src/emails/transactionReceived/html.pug rename to backend/src/emails/templates/transactionReceived/html.pug diff --git a/backend/src/emails/transactionReceived/subject.pug b/backend/src/emails/templates/transactionReceived/subject.pug similarity index 100% rename from backend/src/emails/transactionReceived/subject.pug rename to backend/src/emails/templates/transactionReceived/subject.pug From 3d55c7da9e7ffc1435fece650e760b9de04a597d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 13:41:42 +0100 Subject: [PATCH 45/66] Fix production by copying missing email templates and locale files into the build folder --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 519f9e6c0..c6b3dabc2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -8,7 +8,7 @@ "license": "Apache-2.0", "private": false, "scripts": { - "build": "tsc --build", + "build": "tsc --build && mkdir -p build/src/emails/templates/ && cp -r src/emails/templates/* build/src/emails/templates/ && mkdir -p build/src/locales/ && cp -r src/locales/*.json build/src/locales/", "clean": "tsc --build --clean", "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts", From a7ee30fc3fd444ad28229c586d50939a302ac768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 13:42:37 +0100 Subject: [PATCH 46/66] Fix some comments and readme spellings --- DOCKER_MORE_CLOSELY.md | 4 ++-- docker-compose.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DOCKER_MORE_CLOSELY.md b/DOCKER_MORE_CLOSELY.md index f2aae81c7..c21b99829 100644 --- a/DOCKER_MORE_CLOSELY.md +++ b/DOCKER_MORE_CLOSELY.md @@ -4,7 +4,7 @@ ***Attention:** For using Docker commands in Apple M1 environments!* -### Enviroment Variable For Apple M1 Platform +### Environment Variable For Apple M1 Platform To set the Docker platform environment variable in your terminal tab, run: @@ -27,7 +27,7 @@ $ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker- $ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up ``` -## Analysing Docker Builds +## Analyzing Docker Builds To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it! diff --git a/docker-compose.yml b/docker-compose.yml index 5f0ab4dde..5eea075c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -108,7 +108,7 @@ services: #env_file: # - ./frontend/.env volumes: - # : – mirror bidirectional path in local context with path in Docker container + # : – mirror bidirectional path in local context with path in Docker container - ./logs/backend:/logs/backend ######################################################## From 317224db7a08027531f489e32476983f1e80091a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Tue, 29 Nov 2022 14:09:15 +0100 Subject: [PATCH 47/66] Refactor some readme's by moving their content into 'CONTRIBUTING.md' --- DOCKER_MORE_CLOSELY.md => CONTRIBUTING.md | 28 +++++++++++++++++++---- admin/src/locales/README.md | 3 --- backend/src/locales/README.md | 3 --- frontend/src/locales/README.md | 25 -------------------- 4 files changed, 23 insertions(+), 36 deletions(-) rename DOCKER_MORE_CLOSELY.md => CONTRIBUTING.md (65%) delete mode 100644 admin/src/locales/README.md delete mode 100644 backend/src/locales/README.md delete mode 100644 frontend/src/locales/README.md diff --git a/DOCKER_MORE_CLOSELY.md b/CONTRIBUTING.md similarity index 65% rename from DOCKER_MORE_CLOSELY.md rename to CONTRIBUTING.md index c21b99829..3ebcdb73a 100644 --- a/DOCKER_MORE_CLOSELY.md +++ b/CONTRIBUTING.md @@ -1,10 +1,28 @@ -# Docker More Closely +# Contributing -## Apple M1 Platform +If you contribute to our project, please consider the following points. + +## Localization + +### Quotation Marks + +The following characters are different from the programming quotation mark: + +`"` or `\"` + +Please copy and paste the following quotes for the languages: + +- de: „Dies ist ein Beispielsatz.“ +- en: “This is a sample sentence.” + - See + +## Docker – More Closely + +### Apple M1 Platform ***Attention:** For using Docker commands in Apple M1 environments!* -### Environment Variable For Apple M1 Platform +#### Environment Variable For Apple M1 Platform To set the Docker platform environment variable in your terminal tab, run: @@ -13,7 +31,7 @@ To set the Docker platform environment variable in your terminal tab, run: $ export DOCKER_DEFAULT_PLATFORM=linux/amd64 ``` -### Docker Compose Override File For Apple M1 Platform +#### Docker Compose Override File For Apple M1 Platform For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: @@ -27,7 +45,7 @@ $ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker- $ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up ``` -## Analyzing Docker Builds +### Analyzing Docker Builds To analyze a Docker build, there is a wonderful tool called [dive](https://github.com/wagoodman/dive). Please sponsor if you're using it! diff --git a/admin/src/locales/README.md b/admin/src/locales/README.md deleted file mode 100644 index 5d6bf75b1..000000000 --- a/admin/src/locales/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Localizations - -Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/backend/src/locales/README.md b/backend/src/locales/README.md deleted file mode 100644 index 5d6bf75b1..000000000 --- a/backend/src/locales/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Localizations - -Please see [frontend localization](/frontend/src/locales/README.md). diff --git a/frontend/src/locales/README.md b/frontend/src/locales/README.md deleted file mode 100644 index 2c03abbd4..000000000 --- a/frontend/src/locales/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Localizations - -## Quotation Marks - -The following characters are different from the programming quotation mark: - -`"` - -### English - -In English, we use these double-barreled quotation marks: - -“This is a sample sentence.” - -Please copy and paste … - -See - -### German - -In German, we use these double-barreled quotation marks: - -„Dies ist ein Beispielsatz.“ - -Please copy and paste … From 10ad42babe2dde7f37b2c8f955f19668c0c8eb97 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Tue, 29 Nov 2022 16:37:25 +0100 Subject: [PATCH 48/66] missing files to be copied --- backend/Dockerfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/Dockerfile b/backend/Dockerfile index 6225a4cd7..c09e5aaf8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -98,10 +98,18 @@ COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build # We also copy the node_modules express and serve-static for the run script COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules + # Copy static files # COPY --from=build ${DOCKER_WORKDIR}/public ./public # Copy package.json for script definitions (lock file should not be needed) COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json +# Copy tsconfig.json to provide alias path definitions +COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json +# Copy log4js-config.json to provide log configuration +COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json +# Copy memonic type since its referenced in the sources +# TODO: remove +COPY --from=build ${DOCKER_WORKDIR}/src/config/mnemonic.uncompressed_buffer13116.txt ./src/config/mnemonic.uncompressed_buffer13116.txt # Copy run scripts run/ # COPY --from=build ${DOCKER_WORKDIR}/run ./run From c0096aa26c97d89439e5b09d317508a318cd6143 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Nov 2022 12:15:47 +0100 Subject: [PATCH 49/66] fix(backend): critical bug --- .../resolver/TransactionResolver.test.ts | 8 ++------ .../graphql/resolver/TransactionResolver.ts | 19 ++++--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index f4315d359..1d4fe5708 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -291,7 +291,6 @@ describe('send coins', () => { await cleanDB() }) - /* describe('trying to send negative amount', () => { it('throws an error', async () => { expect( @@ -305,18 +304,15 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], + errors: [new GraphQLError(`Amount to send must be positive`)], }), ) }) it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - `user hasn't enough GDD or amount is < 0 : balance=null`, - ) + expect(logger.error).toBeCalledWith(`Amount to send must be positive`) }) }) - */ describe('good transaction', () => { it('sends the coins', async () => { diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 594039cfd..3dbd4afb9 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -314,6 +314,10 @@ export class TransactionResolver { @Ctx() context: Context, ): Promise { logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) + if (amount.lte(0)) { + logger.error(`Amount to send must be positive`) + throw new Error('Amount to send must be positive') + } // TODO this is subject to replay attacks const senderUser = getUser(context) @@ -324,22 +328,7 @@ export class TransactionResolver { // validate recipient user const recipientUser = await findUserByEmail(email) - /* - const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) - if (!emailContact) { - logger.error(`Could not find UserContact with email: ${email}`) - throw new Error(`Could not find UserContact with email: ${email}`) - } - */ - // const recipientUser = await dbUser.findOne({ id: emailContact.userId }) - /* Code inside this if statement is unreachable (useless by so), - in findUserByEmail() an error is already thrown if the user is not found - */ - if (!recipientUser) { - logger.error(`unknown recipient to UserContact: email=${email}`) - throw new Error('unknown recipient') - } if (recipientUser.deletedAt) { logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) throw new Error('The recipient account was deleted') From bc20bfa8f6e38e40fbcc67c692e926ad28fae1f0 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Nov 2022 12:46:46 +0100 Subject: [PATCH 50/66] fix(backend): delete / undelete email contact as well --- .../graphql/resolver/AdminResolver.test.ts | 22 +++++++++++++++++++ backend/src/graphql/resolver/AdminResolver.ts | 9 ++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 503bab472..757f552f8 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -366,6 +366,19 @@ describe('AdminResolver', () => { expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date)) }) + it('has deleted at set in users and user contacts', async () => { + await expect( + User.findOneOrFail({ + where: { id: user.id }, + withDeleted: true, + relations: ['emailContact'], + }), + ).resolves.toMatchObject({ + deletedAt: expect.any(Date), + emailContact: expect.objectContaining({ deletedAt: expect.any(Date) }), + }) + }) + describe('delete deleted user', () => { it('throws an error', async () => { jest.clearAllMocks() @@ -489,6 +502,15 @@ describe('AdminResolver', () => { }), ) }) + + it('has deleted at set to null in users and user contacts', async () => { + await expect( + User.findOneOrFail({ where: { id: user.id }, relations: ['emailContact'] }), + ).resolves.toMatchObject({ + deletedAt: null, + emailContact: expect.objectContaining({ deletedAt: null }), + }) + }) }) }) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 80c69a864..9ff4824e5 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -200,7 +200,7 @@ export class AdminResolver { @Arg('userId', () => Int) userId: number, @Ctx() context: Context, ): Promise { - const user = await dbUser.findOne({ id: userId }) + const user = await dbUser.findOne({ where: { id: userId }, relations: ['emailContact'] }) // user exists ? if (!user) { logger.error(`Could not find user with userId: ${userId}`) @@ -214,6 +214,7 @@ export class AdminResolver { } // soft-delete user await user.softRemove() + await user.emailContact.softRemove() const newUser = await dbUser.findOne({ id: userId }, { withDeleted: true }) return newUser ? newUser.deletedAt : null } @@ -221,7 +222,10 @@ export class AdminResolver { @Authorized([RIGHTS.UNDELETE_USER]) @Mutation(() => Date, { nullable: true }) async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise { - const user = await dbUser.findOne({ id: userId }, { withDeleted: true }) + const user = await dbUser.findOne( + { id: userId }, + { withDeleted: true, relations: ['emailContact'] }, + ) if (!user) { logger.error(`Could not find user with userId: ${userId}`) throw new Error(`Could not find user with userId: ${userId}`) @@ -231,6 +235,7 @@ export class AdminResolver { throw new Error('User is not deleted') } await user.recover() + await user.emailContact.recover() return null } From bbd163f1e05a589384e63d66c2adf5f6c62a448c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Nov 2022 14:34:09 +0100 Subject: [PATCH 51/66] integrate export const checkEmailVerificationCode = async ( --- backend/src/graphql/resolver/UserResolver.ts | 66 +++++++------------- 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 087654269..2e294196a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -141,35 +141,6 @@ const newEmailContact = (email: string, userId: number): DbUserContact => { return emailContact } -export const checkEmailVerificationCode = async ( - emailContact: DbUserContact, - optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER, -): Promise => { - logger.info(`checkEmailVerificationCode... ${emailContact}`) - if (!canEmailResend(emailContact.updatedAt || emailContact.createdAt)) { - logger.error( - `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, - ) - throw new Error( - `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, - ) - } - emailContact.updatedAt = new Date() - emailContact.emailResendCount++ - emailContact.emailVerificationCode = random(64) - emailContact.emailOptInTypeId = optInType - await DbUserContact.save(emailContact).catch(() => { - logger.error('Unable to save email verification code= ' + emailContact) - throw new Error('Unable to save email verification code.') - }) - logger.info(`checkEmailVerificationCode...successful: ${emailContact}`) - return emailContact -} - export const activationLink = (verificationCode: BigInt): string => { logger.debug(`activationLink(${verificationCode})...`) return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, verificationCode.toString()) @@ -492,21 +463,32 @@ export class UserResolver { return true } - // can be both types: REGISTER and RESET_PASSWORD - // let optInCode = await LoginEmailOptIn.findOne({ - // userId: user.id, - // }) - // let optInCode = user.emailContact.emailVerificationCode - const dbUserContact = await checkEmailVerificationCode( - user.emailContact, - OptInType.EMAIL_OPT_IN_RESET_PASSWORD, - ) + if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) { + logger.error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) + throw new Error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) + } + + user.emailContact.updatedAt = new Date() + user.emailContact.emailResendCount++ + user.emailContact.emailVerificationCode = random(64) + user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD + await user.emailContact.save().catch(() => { + logger.error('Unable to save email verification code= ' + user.emailContact) + throw new Error('Unable to save email verification code.') + }) - // optInCode = await checkOptInCode(optInCode, user, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) - logger.info(`optInCode for ${email}=${dbUserContact}`) + logger.info(`optInCode for ${email}=${user.emailContact}`) // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendResetPasswordEmailMailer({ - link: activationLink(dbUserContact.emailVerificationCode), + link: activationLink(user.emailContact.emailVerificationCode), firstName: user.firstName, lastName: user.lastName, email, @@ -516,7 +498,7 @@ export class UserResolver { /* uncomment this, when you need the activation link on the console */ // In case EMails are disabled log the activation link for the user if (!emailSent) { - logger.debug(`Reset password link: ${activationLink(dbUserContact.emailVerificationCode)}`) + logger.debug(`Reset password link: ${activationLink(user.emailContact.emailVerificationCode)}`) } logger.info(`forgotPassword(${email}) successful...`) From 796f5af2c6d76d6cdabe8de85f4074d601ce387f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 30 Nov 2022 14:45:02 +0100 Subject: [PATCH 52/66] remove unused function, count resent by admin --- backend/src/graphql/resolver/AdminResolver.ts | 3 +++ backend/src/graphql/resolver/UserResolver.ts | 14 ++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 80c69a864..40b7ae176 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -654,6 +654,9 @@ export class AdminResolver { throw new Error(`The emailContact: ${email} of htis User is deleted.`) } + emailContact.emailResendCount++ + await emailContact.save() + // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ link: activationLink(emailContact.emailVerificationCode), diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2e294196a..1d7cf49da 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -466,16 +466,16 @@ export class UserResolver { if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) { logger.error( `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, ) throw new Error( `email already sent less than ${printTimeDuration( - CONFIG.EMAIL_CODE_REQUEST_TIME, - )} minutes ago`, + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, ) } - + user.emailContact.updatedAt = new Date() user.emailContact.emailResendCount++ user.emailContact.emailVerificationCode = random(64) @@ -498,7 +498,9 @@ export class UserResolver { /* uncomment this, when you need the activation link on the console */ // In case EMails are disabled log the activation link for the user if (!emailSent) { - logger.debug(`Reset password link: ${activationLink(user.emailContact.emailVerificationCode)}`) + logger.debug( + `Reset password link: ${activationLink(user.emailContact.emailVerificationCode)}`, + ) } logger.info(`forgotPassword(${email}) successful...`) From 46014adc6d057496c4169d6591ccbfae01929fe3 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 1 Dec 2022 12:07:25 +0100 Subject: [PATCH 53/66] Database join contribution to transaction table, add new join to query. --- backend/src/graphql/model/Transaction.ts | 4 +- .../resolver/TransactionLinkResolver.ts | 3 +- .../graphql/resolver/TransactionResolver.ts | 3 +- backend/src/typeorm/repository/Transaction.ts | 17 ++++ backend/src/util/virtualTransactions.ts | 2 + .../Contribution.ts | 6 ++ .../Transaction.ts | 99 +++++++++++++++++++ database/entity/Transaction.ts | 2 +- 8 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 database/entity/0052-add_updated_at_to_contributions/Transaction.ts diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 24c66ac67..09379e0eb 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -42,7 +42,9 @@ export class Transaction { this.creationDate = transaction.creationDate this.linkedUser = linkedUser this.linkedTransactionId = transaction.linkedTransactionId - this.transactionLinkId = transaction.transactionLinkId + this.transactionLinkId = transaction.contribution + ? transaction.contribution.contributionLinkId + : transaction.transactionLinkId } @Field(() => Number) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 8d648bc65..a3c170999 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -278,6 +278,7 @@ export class TransactionLinkResolver { .createQueryBuilder() .select('transaction') .from(DbTransaction, 'transaction') + .innerJoinAndSelect('transaction.contribution', 'c') .where('transaction.userId = :id', { id: user.id }) .orderBy('transaction.balanceDate', 'DESC') .getOne() @@ -301,7 +302,7 @@ export class TransactionLinkResolver { transaction.balanceDate = now transaction.decay = decay ? decay.decay : new Decimal(0) transaction.decayStart = decay ? decay.start : null - transaction.transactionLinkId = contributionLink.id + transaction.contribution = contribution await queryRunner.manager.insert(DbTransaction, transaction) contribution.confirmedAt = now diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f0fb2f452..e89870558 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -215,7 +215,7 @@ export class TransactionResolver { // find current balance const lastTransaction = await dbTransaction.findOne( { userId: user.id }, - { order: { balanceDate: 'DESC' } }, + { order: { balanceDate: 'DESC' }, relations: ['contribution'] }, ) logger.debug(`lastTransaction=${lastTransaction}`) @@ -238,7 +238,6 @@ export class TransactionResolver { order, ) context.transactionCount = userTransactionsCount - // find involved users; I am involved const involvedUserIds: number[] = [user.id] userTransactions.forEach((transaction: dbTransaction) => { diff --git a/backend/src/typeorm/repository/Transaction.ts b/backend/src/typeorm/repository/Transaction.ts index f84b57626..7952be0fc 100644 --- a/backend/src/typeorm/repository/Transaction.ts +++ b/backend/src/typeorm/repository/Transaction.ts @@ -1,4 +1,5 @@ import { EntityRepository, Repository } from '@dbTools/typeorm' +import { Contribution } from '@entity/Contribution' import { Transaction } from '@entity/Transaction' import { Order } from '@enum/Order' import { TransactionTypeId } from '@enum/TransactionTypeId' @@ -14,6 +15,11 @@ export class TransactionRepository extends Repository { ): Promise<[Transaction[], number]> { if (onlyCreation) { return this.createQueryBuilder('userTransaction') + .innerJoinAndSelect( + 'userTransaction.contribution', + 'c', + 'userTransaction.id = c.transactionId', + ) .where('userTransaction.userId = :userId', { userId }) .andWhere('userTransaction.typeId = :typeId', { typeId: TransactionTypeId.CREATION, @@ -24,6 +30,11 @@ export class TransactionRepository extends Repository { .getManyAndCount() } return this.createQueryBuilder('userTransaction') + .innerJoinAndSelect( + 'userTransaction.contribution', + 'c', + 'userTransaction.id = c.transactionId', + ) .where('userTransaction.userId = :userId', { userId }) .orderBy('userTransaction.balanceDate', order) .limit(limit) @@ -33,6 +44,12 @@ export class TransactionRepository extends Repository { findLastForUser(userId: number): Promise { return this.createQueryBuilder('userTransaction') + .innerJoinAndMapOne( + 'userTransaction.contribution', + Contribution, + 'c', + 'userTransaction.id = c.transactionId', + ) .where('userTransaction.userId = :userId', { userId }) .orderBy('userTransaction.balanceDate', 'DESC') .getOne() diff --git a/backend/src/util/virtualTransactions.ts b/backend/src/util/virtualTransactions.ts index 08d44b48d..b02f87ee5 100644 --- a/backend/src/util/virtualTransactions.ts +++ b/backend/src/util/virtualTransactions.ts @@ -49,6 +49,7 @@ const virtualLinkTransaction = ( decay: decay.toDecimalPlaces(2, Decimal.ROUND_FLOOR), memo: '', creationDate: null, + contribution: null, ...defaultModelFunctions, } return new Transaction(linkDbTransaction, user) @@ -78,6 +79,7 @@ const virtualDecayTransaction = ( decayStart: decay.start, memo: '', creationDate: null, + contribution: null, ...defaultModelFunctions, } return new Transaction(decayDbTransaction, user) diff --git a/database/entity/0052-add_updated_at_to_contributions/Contribution.ts b/database/entity/0052-add_updated_at_to_contributions/Contribution.ts index 2242a753f..4676c14af 100644 --- a/database/entity/0052-add_updated_at_to_contributions/Contribution.ts +++ b/database/entity/0052-add_updated_at_to_contributions/Contribution.ts @@ -8,10 +8,12 @@ import { JoinColumn, ManyToOne, OneToMany, + OneToOne, } from 'typeorm' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' import { User } from '../User' import { ContributionMessage } from '../ContributionMessage' +import { Transaction } from '../Transaction' @Entity('contributions') export class Contribution extends BaseEntity { @@ -92,4 +94,8 @@ export class Contribution extends BaseEntity { @OneToMany(() => ContributionMessage, (message) => message.contribution) @JoinColumn({ name: 'contribution_id' }) messages?: ContributionMessage[] + + @OneToOne(() => Transaction, (transaction) => transaction.contribution) + @JoinColumn({ name: 'transaction_id' }) + transaction?: Transaction | null } diff --git a/database/entity/0052-add_updated_at_to_contributions/Transaction.ts b/database/entity/0052-add_updated_at_to_contributions/Transaction.ts new file mode 100644 index 000000000..ef8d0abdc --- /dev/null +++ b/database/entity/0052-add_updated_at_to_contributions/Transaction.ts @@ -0,0 +1,99 @@ +import Decimal from 'decimal.js-light' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' +import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Contribution } from '../Contribution' + +@Entity('transactions') +export class Transaction extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ name: 'user_id', unsigned: true, nullable: false }) + userId: number + + @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null }) + previous: number | null + + @Column({ name: 'type_id', unsigned: true, nullable: false }) + typeId: number + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + amount: Decimal + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + balance: Decimal + + @Column({ + name: 'balance_date', + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + nullable: false, + }) + balanceDate: Date + + @Column({ + type: 'decimal', + precision: 40, + scale: 20, + nullable: false, + transformer: DecimalTransformer, + }) + decay: Decimal + + @Column({ + name: 'decay_start', + type: 'datetime', + nullable: true, + default: null, + }) + decayStart: Date | null + + @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) + memo: string + + @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null }) + creationDate: Date | null + + @Column({ + name: 'linked_user_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedUserId?: number | null + + @Column({ + name: 'linked_transaction_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + linkedTransactionId?: number | null + + @Column({ + name: 'transaction_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + transactionLinkId?: number | null + + @OneToOne(() => Contribution, (contribution) => contribution.transaction) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + contribution?: Contribution | null +} diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index 5365b0f70..b67dfaa97 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0036-unique_previous_in_transactions/Transaction' +export { Transaction } from './0052-add_updated_at_to_contributions/Transaction' From ec396e85219889322bfbd7800e28cb3330b350c3 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 1 Dec 2022 12:45:15 +0100 Subject: [PATCH 54/66] Merge master. --- backend/src/graphql/resolver/TransactionResolver.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 25260e963..5fb13ba6b 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -206,7 +206,7 @@ export class TransactionResolver { // find current balance const lastTransaction = await dbTransaction.findOne( { userId: user.id }, - { order: { balanceDate: 'DESC' }, relations: ['contribution'] }, + { order: { balanceDate: 'DESC' } }, ) logger.debug(`lastTransaction=${lastTransaction}`) @@ -313,10 +313,6 @@ export class TransactionResolver { @Ctx() context: Context, ): Promise { logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) - if (amount.lte(0)) { - logger.error(`Amount to send must be positive`) - throw new Error('Amount to send must be positive') - } // TODO this is subject to replay attacks const senderUser = getUser(context) @@ -327,7 +323,6 @@ export class TransactionResolver { // validate recipient user const recipientUser = await findUserByEmail(email) - if (recipientUser.deletedAt) { logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) throw new Error('The recipient account was deleted') From 4a27fa07f0de17732c529e7b0c4dbc3c55b5fba3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 1 Dec 2022 14:49:56 +0100 Subject: [PATCH 55/66] fix(database): consistent deleted at bewteen users and user contacts --- .../0055-consistent_deleted_users.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 database/migrations/0055-consistent_deleted_users.ts diff --git a/database/migrations/0055-consistent_deleted_users.ts b/database/migrations/0055-consistent_deleted_users.ts new file mode 100644 index 000000000..e4f2df87f --- /dev/null +++ b/database/migrations/0055-consistent_deleted_users.ts @@ -0,0 +1,26 @@ +/* MIGRATION TO soft delete user contacts of soft deleted 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>) { + const contactsToFix = await queryFn(` + SELECT user_contacts.id, users.deleted_at + FROM user_contacts JOIN users ON users.email_id = user_contacts.id + WHERE user_contacts.deleted_at IS NULL + AND user_id IN (SELECT id FROM users WHERE deleted_at IS NOT NULL);`) + + for (let i = 0; i < contactsToFix.length; i++) { + const deletedAt = new Date(contactsToFix[i].deleted_at) + .toISOString() + .slice(0, 19) + .replace('T', ' ') + + await queryFn(` + UPDATE user_contacts SET deleted_at = '${deletedAt}' WHERE id = ${contactsToFix[i].id};`) + } +} + +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable-next-line @typescript-eslint/no-unused-vars */ +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) {} From 7f1e69c1bdf0e109c9a48b1a254558e17b5e45d5 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 1 Dec 2022 14:52:08 +0100 Subject: [PATCH 56/66] update database version --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index c9e5ea79f..ee99ef809 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0054-recalculate_balance_and_decay', + DB_VERSION: '0055-consistent_deleted_users', 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 From 0b2887b86fb90ddff3ebb9177d1413f544d59c02 Mon Sep 17 00:00:00 2001 From: elweyn Date: Thu, 1 Dec 2022 15:21:49 +0100 Subject: [PATCH 57/66] Correct joins between contribution and transaction --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 5 +---- backend/src/graphql/resolver/TransactionResolver.ts | 6 +++++- backend/src/typeorm/repository/Transaction.ts | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 79bfd375e..f831b7ff1 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -265,6 +265,7 @@ export class TransactionLinkResolver { const creations = await getUserCreation(user.id, clientTimezoneOffset) logger.info('open creations', creations) validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset) + const contribution = new DbContribution() contribution.userId = user.id contribution.createdAt = now @@ -274,19 +275,16 @@ export class TransactionLinkResolver { contribution.contributionLinkId = contributionLink.id contribution.contributionType = ContributionType.LINK contribution.contributionStatus = ContributionStatus.CONFIRMED - await queryRunner.manager.insert(DbContribution, contribution) const lastTransaction = await queryRunner.manager .createQueryBuilder() .select('transaction') .from(DbTransaction, 'transaction') - .innerJoinAndSelect('transaction.contribution', 'c') .where('transaction.userId = :id', { id: user.id }) .orderBy('transaction.balanceDate', 'DESC') .getOne() let newBalance = new Decimal(0) - let decay: Decay | null = null if (lastTransaction) { decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now) @@ -305,7 +303,6 @@ export class TransactionLinkResolver { transaction.balanceDate = now transaction.decay = decay ? decay.decay : new Decimal(0) transaction.decayStart = decay ? decay.start : null - transaction.contribution = contribution await queryRunner.manager.insert(DbTransaction, transaction) contribution.confirmedAt = now diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 5fb13ba6b..7ec925181 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -206,7 +206,7 @@ export class TransactionResolver { // find current balance const lastTransaction = await dbTransaction.findOne( { userId: user.id }, - { order: { balanceDate: 'DESC' } }, + { order: { balanceDate: 'DESC' }, relations: ['contribution'] }, ) logger.debug(`lastTransaction=${lastTransaction}`) @@ -313,6 +313,10 @@ export class TransactionResolver { @Ctx() context: Context, ): Promise { logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`) + if (amount.lte(0)) { + logger.error(`Amount to send must be positive`) + throw new Error('Amount to send must be positive') + } // TODO this is subject to replay attacks const senderUser = getUser(context) diff --git a/backend/src/typeorm/repository/Transaction.ts b/backend/src/typeorm/repository/Transaction.ts index 7952be0fc..943ad1081 100644 --- a/backend/src/typeorm/repository/Transaction.ts +++ b/backend/src/typeorm/repository/Transaction.ts @@ -15,7 +15,7 @@ export class TransactionRepository extends Repository { ): Promise<[Transaction[], number]> { if (onlyCreation) { return this.createQueryBuilder('userTransaction') - .innerJoinAndSelect( + .leftJoinAndSelect( 'userTransaction.contribution', 'c', 'userTransaction.id = c.transactionId', @@ -30,7 +30,7 @@ export class TransactionRepository extends Repository { .getManyAndCount() } return this.createQueryBuilder('userTransaction') - .innerJoinAndSelect( + .leftJoinAndSelect( 'userTransaction.contribution', 'c', 'userTransaction.id = c.transactionId', @@ -44,7 +44,7 @@ export class TransactionRepository extends Repository { findLastForUser(userId: number): Promise { return this.createQueryBuilder('userTransaction') - .innerJoinAndMapOne( + .leftJoinAndMapOne( 'userTransaction.contribution', Contribution, 'c', From 41495c2c654e9b8ff260a341b748441831defca1 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 1 Dec 2022 15:53:09 +0100 Subject: [PATCH 58/66] fix(backend): run all timers for high values --- backend/src/graphql/resolver/util/creations.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts index 8d747e989..6be317e16 100644 --- a/backend/src/graphql/resolver/util/creations.test.ts +++ b/backend/src/graphql/resolver/util/creations.test.ts @@ -170,8 +170,11 @@ describe('util/creation', () => { const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0) beforeAll(() => { + const halfMsToRun = (targetDate.getTime() - now.getTime()) / 2 jest.useFakeTimers() - setTimeout(jest.fn(), targetDate.getTime() - now.getTime()) + setTimeout(jest.fn(), halfMsToRun) + jest.runAllTimers() + setTimeout(jest.fn(), halfMsToRun) jest.runAllTimers() }) @@ -225,8 +228,10 @@ describe('util/creation', () => { }) it('has the clock set correctly', () => { + const targetMonth = nextMonthTargetDate.getMonth() + 1 + const targetMonthString = (targetMonth < 10 ? '0' : '') + String(targetMonth) expect(new Date().toISOString()).toContain( - `${nextMonthTargetDate.getFullYear()}-${nextMonthTargetDate.getMonth() + 1}-01T01:`, + `${nextMonthTargetDate.getFullYear()}-${targetMonthString}-01T01:`, ) }) From d3678bb81c51672ceee0d07359a76500eac6cec3 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 1 Dec 2022 17:44:56 +0100 Subject: [PATCH 59/66] feat(backend): log client timezone offset --- backend/src/graphql/resolver/util/creations.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index eb4b6394d..54286d2aa 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -103,6 +103,9 @@ export const getUserCreation = async ( const getCreationMonths = (timezoneOffset: number): number[] => { const clientNow = new Date() clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000) + logger.info( + `getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`, + ) return [ new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1).getMonth() + 1, new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1).getMonth() + 1, From 3ba591637cd883ad186c49661b718d7a6f6d9c45 Mon Sep 17 00:00:00 2001 From: elweyn Date: Fri, 2 Dec 2022 07:32:08 +0100 Subject: [PATCH 60/66] Remove unused method. --- backend/src/typeorm/repository/Transaction.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/backend/src/typeorm/repository/Transaction.ts b/backend/src/typeorm/repository/Transaction.ts index 943ad1081..affed5f42 100644 --- a/backend/src/typeorm/repository/Transaction.ts +++ b/backend/src/typeorm/repository/Transaction.ts @@ -1,5 +1,4 @@ import { EntityRepository, Repository } from '@dbTools/typeorm' -import { Contribution } from '@entity/Contribution' import { Transaction } from '@entity/Transaction' import { Order } from '@enum/Order' import { TransactionTypeId } from '@enum/TransactionTypeId' @@ -41,17 +40,4 @@ export class TransactionRepository extends Repository { .offset(offset) .getManyAndCount() } - - findLastForUser(userId: number): Promise { - return this.createQueryBuilder('userTransaction') - .leftJoinAndMapOne( - 'userTransaction.contribution', - Contribution, - 'c', - 'userTransaction.id = c.transactionId', - ) - .where('userTransaction.userId = :userId', { userId }) - .orderBy('userTransaction.balanceDate', 'DESC') - .getOne() - } } From ffb5988760108f9df39eb1f8bb804d011890fab4 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 6 Dec 2022 11:58:17 +0100 Subject: [PATCH 61/66] Refactoring of some small smelling code. --- .../resolver/TransactionLinkResolver.ts | 3 +- .../graphql/resolver/TransactionResolver.ts | 1 + backend/src/typeorm/repository/Transaction.ts | 30 +++--- .../Transaction.ts | 7 +- .../Transaction.ts | 99 ------------------- database/entity/Transaction.ts | 2 +- 6 files changed, 21 insertions(+), 121 deletions(-) delete mode 100644 database/entity/0052-add_updated_at_to_contributions/Transaction.ts diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index f831b7ff1..1b3558bb2 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -265,7 +265,6 @@ export class TransactionLinkResolver { const creations = await getUserCreation(user.id, clientTimezoneOffset) logger.info('open creations', creations) validateContribution(creations, contributionLink.amount, now, clientTimezoneOffset) - const contribution = new DbContribution() contribution.userId = user.id contribution.createdAt = now @@ -275,6 +274,7 @@ export class TransactionLinkResolver { contribution.contributionLinkId = contributionLink.id contribution.contributionType = ContributionType.LINK contribution.contributionStatus = ContributionStatus.CONFIRMED + await queryRunner.manager.insert(DbContribution, contribution) const lastTransaction = await queryRunner.manager @@ -285,6 +285,7 @@ export class TransactionLinkResolver { .orderBy('transaction.balanceDate', 'DESC') .getOne() let newBalance = new Decimal(0) + let decay: Decay | null = null if (lastTransaction) { decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, now) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 7ec925181..f2edda2c1 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -229,6 +229,7 @@ export class TransactionResolver { order, ) context.transactionCount = userTransactionsCount + // find involved users; I am involved const involvedUserIds: number[] = [user.id] userTransactions.forEach((transaction: dbTransaction) => { diff --git a/backend/src/typeorm/repository/Transaction.ts b/backend/src/typeorm/repository/Transaction.ts index affed5f42..58e9ca30b 100644 --- a/backend/src/typeorm/repository/Transaction.ts +++ b/backend/src/typeorm/repository/Transaction.ts @@ -12,29 +12,21 @@ export class TransactionRepository extends Repository { order: Order, onlyCreation?: boolean, ): Promise<[Transaction[], number]> { - if (onlyCreation) { - return this.createQueryBuilder('userTransaction') - .leftJoinAndSelect( - 'userTransaction.contribution', - 'c', - 'userTransaction.id = c.transactionId', - ) - .where('userTransaction.userId = :userId', { userId }) - .andWhere('userTransaction.typeId = :typeId', { - typeId: TransactionTypeId.CREATION, - }) - .orderBy('userTransaction.balanceDate', order) - .limit(limit) - .offset(offset) - .getManyAndCount() - } - return this.createQueryBuilder('userTransaction') + const query = this.createQueryBuilder('userTransaction') .leftJoinAndSelect( 'userTransaction.contribution', - 'c', - 'userTransaction.id = c.transactionId', + 'contribution', + 'userTransaction.id = contribution.transactionId', ) .where('userTransaction.userId = :userId', { userId }) + + if (onlyCreation) { + query.andWhere('userTransaction.typeId = :typeId', { + typeId: TransactionTypeId.CREATION, + }) + } + + return query .orderBy('userTransaction.balanceDate', order) .limit(limit) .offset(offset) diff --git a/database/entity/0036-unique_previous_in_transactions/Transaction.ts b/database/entity/0036-unique_previous_in_transactions/Transaction.ts index 99202eee4..ef8d0abdc 100644 --- a/database/entity/0036-unique_previous_in_transactions/Transaction.ts +++ b/database/entity/0036-unique_previous_in_transactions/Transaction.ts @@ -1,6 +1,7 @@ import Decimal from 'decimal.js-light' -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' +import { Contribution } from '../Contribution' @Entity('transactions') export class Transaction extends BaseEntity { @@ -91,4 +92,8 @@ export class Transaction extends BaseEntity { default: null, }) transactionLinkId?: number | null + + @OneToOne(() => Contribution, (contribution) => contribution.transaction) + @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) + contribution?: Contribution | null } diff --git a/database/entity/0052-add_updated_at_to_contributions/Transaction.ts b/database/entity/0052-add_updated_at_to_contributions/Transaction.ts deleted file mode 100644 index ef8d0abdc..000000000 --- a/database/entity/0052-add_updated_at_to_contributions/Transaction.ts +++ /dev/null @@ -1,99 +0,0 @@ -import Decimal from 'decimal.js-light' -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm' -import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' -import { Contribution } from '../Contribution' - -@Entity('transactions') -export class Transaction extends BaseEntity { - @PrimaryGeneratedColumn('increment', { unsigned: true }) - id: number - - @Column({ name: 'user_id', unsigned: true, nullable: false }) - userId: number - - @Column({ type: 'int', unsigned: true, unique: true, nullable: true, default: null }) - previous: number | null - - @Column({ name: 'type_id', unsigned: true, nullable: false }) - typeId: number - - @Column({ - type: 'decimal', - precision: 40, - scale: 20, - nullable: false, - transformer: DecimalTransformer, - }) - amount: Decimal - - @Column({ - type: 'decimal', - precision: 40, - scale: 20, - nullable: false, - transformer: DecimalTransformer, - }) - balance: Decimal - - @Column({ - name: 'balance_date', - type: 'datetime', - default: () => 'CURRENT_TIMESTAMP', - nullable: false, - }) - balanceDate: Date - - @Column({ - type: 'decimal', - precision: 40, - scale: 20, - nullable: false, - transformer: DecimalTransformer, - }) - decay: Decimal - - @Column({ - name: 'decay_start', - type: 'datetime', - nullable: true, - default: null, - }) - decayStart: Date | null - - @Column({ length: 255, nullable: false, collation: 'utf8mb4_unicode_ci' }) - memo: string - - @Column({ name: 'creation_date', type: 'datetime', nullable: true, default: null }) - creationDate: Date | null - - @Column({ - name: 'linked_user_id', - type: 'int', - unsigned: true, - nullable: true, - default: null, - }) - linkedUserId?: number | null - - @Column({ - name: 'linked_transaction_id', - type: 'int', - unsigned: true, - nullable: true, - default: null, - }) - linkedTransactionId?: number | null - - @Column({ - name: 'transaction_link_id', - type: 'int', - unsigned: true, - nullable: true, - default: null, - }) - transactionLinkId?: number | null - - @OneToOne(() => Contribution, (contribution) => contribution.transaction) - @JoinColumn({ name: 'id', referencedColumnName: 'transactionId' }) - contribution?: Contribution | null -} diff --git a/database/entity/Transaction.ts b/database/entity/Transaction.ts index b67dfaa97..5365b0f70 100644 --- a/database/entity/Transaction.ts +++ b/database/entity/Transaction.ts @@ -1 +1 @@ -export { Transaction } from './0052-add_updated_at_to_contributions/Transaction' +export { Transaction } from './0036-unique_previous_in_transactions/Transaction' From d52f7f9590dc9c1a7b842abdab31936ecfe67828 Mon Sep 17 00:00:00 2001 From: elweyn Date: Tue, 6 Dec 2022 12:20:21 +0100 Subject: [PATCH 62/66] change transactionLinkId to linkId. --- backend/src/graphql/model/Transaction.ts | 6 +++--- .../src/components/TransactionRows/AmountAndNameRow.vue | 4 ++-- .../src/components/Transactions/TransactionCreation.vue | 4 ++-- frontend/src/components/Transactions/TransactionReceive.vue | 4 ++-- frontend/src/components/Transactions/TransactionSend.vue | 4 ++-- frontend/src/graphql/queries.js | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/graphql/model/Transaction.ts b/backend/src/graphql/model/Transaction.ts index 09379e0eb..a7329bcef 100644 --- a/backend/src/graphql/model/Transaction.ts +++ b/backend/src/graphql/model/Transaction.ts @@ -42,7 +42,7 @@ export class Transaction { this.creationDate = transaction.creationDate this.linkedUser = linkedUser this.linkedTransactionId = transaction.linkedTransactionId - this.transactionLinkId = transaction.contribution + this.linkId = transaction.contribution ? transaction.contribution.contributionLinkId : transaction.transactionLinkId } @@ -83,7 +83,7 @@ export class Transaction { @Field(() => Number, { nullable: true }) linkedTransactionId?: number | null - // Links to the TransactionLink when transaction was created by a link + // Links to the TransactionLink/ContributionLink when transaction was created by a link @Field(() => Number, { nullable: true }) - transactionLinkId?: number | null + linkId?: number | null } diff --git a/frontend/src/components/TransactionRows/AmountAndNameRow.vue b/frontend/src/components/TransactionRows/AmountAndNameRow.vue index 96a31dcf3..eb68d9f37 100644 --- a/frontend/src/components/TransactionRows/AmountAndNameRow.vue +++ b/frontend/src/components/TransactionRows/AmountAndNameRow.vue @@ -16,7 +16,7 @@ {{ itemText }} - + {{ $t('via_link') }} @@ -82,7 +82,7 @@ export default { type: String, required: true, }, - transactionLinkId: { + linkId: { type: Number, required: false, }, diff --git a/frontend/src/components/Transactions/TransactionReceive.vue b/frontend/src/components/Transactions/TransactionReceive.vue index 8899b3807..389ac9d5d 100644 --- a/frontend/src/components/Transactions/TransactionReceive.vue +++ b/frontend/src/components/Transactions/TransactionReceive.vue @@ -17,7 +17,7 @@ v-on="$listeners" :amount="amount" :linkedUser="linkedUser" - :transactionLinkId="transactionLinkId" + :linkId="linkId" /> @@ -82,7 +82,7 @@ export default { typeId: { type: String, }, - transactionLinkId: { + linkId: { type: Number, required: false, }, diff --git a/frontend/src/components/Transactions/TransactionSend.vue b/frontend/src/components/Transactions/TransactionSend.vue index f9125b89c..c02f230e7 100644 --- a/frontend/src/components/Transactions/TransactionSend.vue +++ b/frontend/src/components/Transactions/TransactionSend.vue @@ -17,7 +17,7 @@ v-on="$listeners" :amount="amount" :linkedUser="linkedUser" - :transactionLinkId="transactionLinkId" + :linkId="linkId" /> @@ -83,7 +83,7 @@ export default { type: String, required: true, }, - transactionLinkId: { + linkId: { type: Number, required: false, }, diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 1c910a23e..d261797c2 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -45,7 +45,7 @@ export const transactionsQuery = gql` end duration } - transactionLinkId + linkId } } } From bd3f05d6c8c57b1dc010507f115f7e47f72ab986 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 8 Dec 2022 19:11:01 +0100 Subject: [PATCH 63/66] Update backend/src/graphql/resolver/AdminResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 757f552f8..8554f990a 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -366,7 +366,7 @@ describe('AdminResolver', () => { expect(new Date(result.data.deleteUser)).toEqual(expect.any(Date)) }) - it('has deleted at set in users and user contacts', async () => { + it('has deleted_at set in users and user contacts', async () => { await expect( User.findOneOrFail({ where: { id: user.id }, From cbb688112f5f08ed7bb9fb38688dd2773d4b5d41 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 8 Dec 2022 19:11:09 +0100 Subject: [PATCH 64/66] Update backend/src/graphql/resolver/AdminResolver.test.ts Co-authored-by: Ulf Gebhardt --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 8554f990a..4a55bd0ad 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -503,7 +503,7 @@ describe('AdminResolver', () => { ) }) - it('has deleted at set to null in users and user contacts', async () => { + it('has deleted_at set to null in users and user contacts', async () => { await expect( User.findOneOrFail({ where: { id: user.id }, relations: ['emailContact'] }), ).resolves.toMatchObject({ From c449e41cfb6fe3ac86a8b0b0ddfeb24d63a41c09 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 12 Dec 2022 12:45:17 +0100 Subject: [PATCH 65/66] update only by SQL, thanks @ulfgebhardt --- .../0055-consistent_deleted_users.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/database/migrations/0055-consistent_deleted_users.ts b/database/migrations/0055-consistent_deleted_users.ts index e4f2df87f..561e0541a 100644 --- a/database/migrations/0055-consistent_deleted_users.ts +++ b/database/migrations/0055-consistent_deleted_users.ts @@ -4,21 +4,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { - const contactsToFix = await queryFn(` - SELECT user_contacts.id, users.deleted_at - FROM user_contacts JOIN users ON users.email_id = user_contacts.id + await queryFn(` + UPDATE user_contacts LEFT JOIN users ON users.email_id = user_contacts.id + SET user_contacts.deleted_at = users.deleted_at WHERE user_contacts.deleted_at IS NULL - AND user_id IN (SELECT id FROM users WHERE deleted_at IS NOT NULL);`) - - for (let i = 0; i < contactsToFix.length; i++) { - const deletedAt = new Date(contactsToFix[i].deleted_at) - .toISOString() - .slice(0, 19) - .replace('T', ' ') - - await queryFn(` - UPDATE user_contacts SET deleted_at = '${deletedAt}' WHERE id = ${contactsToFix[i].id};`) - } + AND users.deleted_at IS NOT NULL;`) } /* eslint-disable @typescript-eslint/no-empty-function */ From 02656ee117be29f2486bb4f2337f2d23a6154d10 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 13 Dec 2022 15:49:41 +0100 Subject: [PATCH 66/66] fix tests after merge --- .../graphql/resolver/EmailOptinCodes.test.ts | 1 + .../src/graphql/resolver/UserResolver.test.ts | 24 +++++++++---------- backend/src/graphql/resolver/UserResolver.ts | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/backend/src/graphql/resolver/EmailOptinCodes.test.ts b/backend/src/graphql/resolver/EmailOptinCodes.test.ts index 1cf22850d..d7c0b9bd6 100644 --- a/backend/src/graphql/resolver/EmailOptinCodes.test.ts +++ b/backend/src/graphql/resolver/EmailOptinCodes.test.ts @@ -13,6 +13,7 @@ let testEnv: any CONFIG.EMAIL_CODE_VALID_TIME = 1440 CONFIG.EMAIL_CODE_REQUEST_TIME = 10 +CONFIG.EMAIL = false beforeAll(async () => { testEnv = await testEnvironment() diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index c6535ba2b..053905012 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -25,7 +25,6 @@ import { sendAccountMultiRegistrationEmail, sendResetPasswordEmail, } from '@/emails/sendEmailVariants' -import { activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' @@ -844,17 +843,18 @@ describe('UserResolver', () => { ) }) - it('sends reset password email', () => { - expect(sendResetPasswordEmail).toBeCalledWith({ - firstName: 'Bibi', - lastName: 'Bloxberg', - email: 'bibi@bloxberg.de', - language: 'de', - resetLink: activationLink(emailContact.emailVerificationCode), - timeDurationObject: expect.objectContaining({ - hours: expect.any(Number), - minutes: expect.any(Number), - }), + it('sends reset password email', () => { + expect(sendResetPasswordEmail).toBeCalledWith({ + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + language: 'de', + resetLink: expect.any(String), + timeDurationObject: expect.objectContaining({ + hours: expect.any(Number), + minutes: expect.any(Number), + }), + }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index eebff8344..ed10bb803 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -496,7 +496,7 @@ export class UserResolver { lastName: user.lastName, email, language: user.language, - resetLink: activationLink(dbUserContact.emailVerificationCode), + resetLink: activationLink(user.emailContact.emailVerificationCode), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), })