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/47] 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/47] 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 b9c73c2cf77f2445c2bd86ef6efdce8ad2377e36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 18 Oct 2022 23:12:27 +0200 Subject: [PATCH 03/47] first draft --- docu/RoadMap_2022-2023.md | 137 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docu/RoadMap_2022-2023.md diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md new file mode 100644 index 000000000..8a29fcbb5 --- /dev/null +++ b/docu/RoadMap_2022-2023.md @@ -0,0 +1,137 @@ +# Roadmap 2022 / 2023 + +## unsortierte Sammlung von Themen + +1. backend access layer + + - Refactoring der Resolver-Klassen + - Daten-Zugriffschicht zur Kapselung der DB-Schicht + - Transfer-Datenmodel zum Austausch von Daten zwischen den Schichten + - technisches Transaktion-Handling und Lösung von Deadlocks + - Konzept in Arbeit +2. capturing alias + + - Konzept fertig + - Änderungen in Register- und Login-Prozess +3. Passwort-Verschlüsselung + + - Konzept fertig + - Unabhängigkeit von Email erzeugen + - Änderung der User-Email ermöglichen + - Versionierung der verwendeten Verschlüsselungslogik notwendig +4. Contribution-Categories + + - Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet? + - Regeln auf Categories ermöglichen + - Konzept in Arbeit +5. Statistics / Analysen +6. Subgruppierung / Subcommunities + + - **einfacher Ansatz:** innerhalb der existierenden Community gibt es Untergruppierungen, sprich SubCommunities + + - Einführung eine Community-Tabelle + - In der Community-Tabelle gibt es zunächst eine Haupt-Community, die mehrere Sub-Communities haben kann + - ein User ist in der Haupt-Community unique, kann aber in mehreren SubCommunities sein + - Eine SubCommunity dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User + - Eine SubCommunity hat eigene Moderatoren + - Motivation einer SubCommunity: kleine lokale Gruppen und jeder kennt jeden + - **ToDos**: + - DB-Migration für Community-Tabelle, User-SubCommunity-Zuordnungen, UserRights-Tabelle + - Berechtigungen für SubCommunities + - Register- und Login-Prozess für SubCommunity-Anmeldung anpassen + - Auswahl-Box einer SubCommunity + - createUser mit Zuordnung zur ausgewählten SubCommunity + - Schöpfungsprozess auf angemeldete SubCommunity anpassen + - "Beitrag einreichen"-Dialog auf angemeldete SubCommunity anpassen + - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete SubCommunity anpassen + - "Gemeinschaft"-Dialog auf angemeldete SubCommunity anpassen + - "Mein Profil"-Dialog auf SubCommunities anpassen + - Umzug-Service in andere SubCommunity + - Löschen der Mitgliedschaft zu angemeldeter SubCommunity (Deaktivierung der Zuordnung "User-SubCommunity") + - "Senden"-Dialog mit SubCommunity-Auswahl + - "Transaktion"-Dialog mit Filter auf angemeldeter SubCommunity + - AdminInterface auf angemeldete SubCommunity anpassen + - "Übersicht"-Dialog mit Filter auf angemeldete SubCommunity + - "Nutzersuche"-Dialog mit Filter auf angemeldete SubCommunity + - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete SubComunity + - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete SubCommunity anpassen + - **komplexer Ansatz** + + - DB-Migration + + - Community-Tabelle mit Eintrag für Haupt-Community + - Community-User Zuordnungstabelle + - Account-Tabelle + - Account-User Zuordnungstabelle + - SubCommunity-Verwaltung + + - Neuanlage + - Änderungen + - Berechtigungen (Admin, Moderator, User, ...) + - Konten für 2te und 3te Schöpfung + - User-Community-Verwaltung + + - Zuordnung zu einer SubCommunity bei Register bzw Login + - Umzug in andere SubCommunity + - Eindeutigkeit Community-übergreifend? + - User-Account-Verwaltung + + - Berechtigung auf Accounts anderer User (Treuhander) + - Transaktions- und Schöpfungslogik auf Multi-Community anpassen +7. User-Beziehungen und Favoritenverwaltung + + - User-User-Zuordnung + - aus Tx-Liste die aktuellen Favoriten ermitteln + - Verwaltung von Zuordnungen + - Auswahl + - Berechtigungen + - Gruppierung + - Community-übergreifend + - User-Beziehungen +8. technische Ablösung der Email und Ersatz durch GradidoID +9. Zeitzone + + - User sieht immer seine Locale-Zeit und Monate + - Admin sieht immer UTC-Zeit und Monate + - wichtiges Kriterium für Schöpfung ist das TargetDate + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? + - Contribution-Link aktiviert in Tokyo am Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - Gebucht wird die TX mit creationDate=31.10.2022 22:00:00 UTC, + - die Schöpfung hat creationDate=31.10.2022 22:00:00 UTC und contributionDate=01.11.2022 07:00:00 und neu contributionOffset=+09:00 + - **Prüfung auf -12h <= ClientRequestTime <= +12h** + - original ClientRequestTime in DB speichern + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - 31.10.2022 22:00 +09:00 => 10.2022 + - 01.11.2022 07:00 +09:00 => 11.2022 +10. Layout +11. Manuelle User-Registrierung für Admin + + 1. 10.12.2022 Tag bei den Galliern +12. Dezentralisierung / Federation + + 1. Hyperswarm + 2. + +## Priorisierung + +1. capturing alias +2. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption!!**! +3. Zeitzone +4. User-Beziehungen und Favoritenverwaltung +5. Layout +6. Passwort-Verschlüsselung +7. +8. Subgruppierung / Subcommunities (einfacher Ansatz) +9. Contribution-Categories +10. +11. backend access layer +12. +13. Statistics / Analysen +14. +15. technische Ablösung der Email und Ersatz durch GradidoID +16. +17. Dezentralisierung / Federation + +## Zeitleiste From f0187c130119a4a9711e6b802c6ba47071d69bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 18 Oct 2022 23:55:22 +0200 Subject: [PATCH 04/47] first draft after meeting --- docu/RoadMap_2022-2023.md | 92 ++++++++++++-------------- docu/graphics/RoadMap2022-2023.drawio | 60 +++++++++++++++++ docu/graphics/RoadMap2022-2023.png | Bin 0 -> 64646 bytes 3 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 docu/graphics/RoadMap2022-2023.drawio create mode 100644 docu/graphics/RoadMap2022-2023.png diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md index 8a29fcbb5..26a4ec41e 100644 --- a/docu/RoadMap_2022-2023.md +++ b/docu/RoadMap_2022-2023.md @@ -55,29 +55,6 @@ - "Nutzersuche"-Dialog mit Filter auf angemeldete SubCommunity - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete SubComunity - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete SubCommunity anpassen - - **komplexer Ansatz** - - - DB-Migration - - - Community-Tabelle mit Eintrag für Haupt-Community - - Community-User Zuordnungstabelle - - Account-Tabelle - - Account-User Zuordnungstabelle - - SubCommunity-Verwaltung - - - Neuanlage - - Änderungen - - Berechtigungen (Admin, Moderator, User, ...) - - Konten für 2te und 3te Schöpfung - - User-Community-Verwaltung - - - Zuordnung zu einer SubCommunity bei Register bzw Login - - Umzug in andere SubCommunity - - Eindeutigkeit Community-übergreifend? - - User-Account-Verwaltung - - - Berechtigung auf Accounts anderer User (Treuhander) - - Transaktions- und Schöpfungslogik auf Multi-Community anpassen 7. User-Beziehungen und Favoritenverwaltung - User-User-Zuordnung @@ -89,30 +66,47 @@ - Community-übergreifend - User-Beziehungen 8. technische Ablösung der Email und Ersatz durch GradidoID + + * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird + * Email soll aber im Aussen für User optional noch verwendbar bleiben + * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird 9. Zeitzone - User sieht immer seine Locale-Zeit und Monate - Admin sieht immer UTC-Zeit und Monate - - wichtiges Kriterium für Schöpfung ist das TargetDate - - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! - - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? - - Contribution-Link aktiviert in Tokyo am Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung - - Gebucht wird die TX mit creationDate=31.10.2022 22:00:00 UTC, - - die Schöpfung hat creationDate=31.10.2022 22:00:00 UTC und contributionDate=01.11.2022 07:00:00 und neu contributionOffset=+09:00 - - **Prüfung auf -12h <= ClientRequestTime <= +12h** - - original ClientRequestTime in DB speichern - - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 - - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! - - 31.10.2022 22:00 +09:00 => 10.2022 - - 01.11.2022 07:00 +09:00 => 11.2022 + - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja + - Beispiel: User in Tokyo Locale mit Offest +09:00 + + - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - die Contribution wird gespeichert mit + + - creationDate=31.10.2022 22:00:00 UTC + - contributionDate=01.11.2022 07:00:00 + - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 + - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit + + - creationDate=31.10.2022 22:00:00 UTC + - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** + - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern + - Beispiel für täglichen Contribution-Link während des Monats: + + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - Beispiel für täglichen Contribution-Link am Monatswechsel: + + - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 + - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! 10. Layout 11. Manuelle User-Registrierung für Admin - 1. 10.12.2022 Tag bei den Galliern + - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein 12. Dezentralisierung / Federation - 1. Hyperswarm - 2. + - Hyperswarm + - Authentifizierungs- und Autorisierungs-Handshake + - Inter-Community-Communication ## Priorisierung @@ -122,16 +116,16 @@ 4. User-Beziehungen und Favoritenverwaltung 5. Layout 6. Passwort-Verschlüsselung -7. -8. Subgruppierung / Subcommunities (einfacher Ansatz) -9. Contribution-Categories -10. -11. backend access layer -12. -13. Statistics / Analysen -14. -15. technische Ablösung der Email und Ersatz durch GradidoID -16. -17. Dezentralisierung / Federation +7. Subgruppierung / Subcommunities (einfacher Ansatz) +8. Contribution-Categories +9. backend access layer +10. Statistics / Analysen +11. technische Ablösung der Email und Ersatz durch GradidoID +12. Dezentralisierung / Federation ## Zeitleiste + + + + +![img](./graphics/RoadMap2022-2023.png) diff --git a/docu/graphics/RoadMap2022-2023.drawio b/docu/graphics/RoadMap2022-2023.drawio new file mode 100644 index 000000000..58b8dec94 --- /dev/null +++ b/docu/graphics/RoadMap2022-2023.drawio @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docu/graphics/RoadMap2022-2023.png b/docu/graphics/RoadMap2022-2023.png new file mode 100644 index 0000000000000000000000000000000000000000..3ce8511a346c0cec5a9293a0da6b5ddc6b1d8509 GIT binary patch literal 64646 zcmeFZ2UJwcwl0jwR#1_khy(?ZC?GjYP*6moe5_FuL;zlY^SyeMf8BS+qQ@JM&_R8k6U>MA>*0+NEoTCIW zvxl&9V1`fLV&yDpr*C5_3pIjRf{8}XKc{i9W1ho2YUJ{BgrA-L=YWabukW3_g_9jK zpk(HH`T>q#(;UoRbNvsG6CPz3I;naB^|+{yqIM#LUF>*Qa@TxPRWU(*HI4bSp4Z zeIuyT&lj8wolwpW3I&foJmHEs#1ccjUxfMj+@t@^-QOFqgIJ$_{Qu>F(}lt8U4HEj zVgxpOdT9-{277*BYHwu;*2n>V1@E(=fQx^z^3x$0)WOd1bk^y?vDddVft>D-5A&v9=M4h2f`Ih_df7oN z_3h0Z|MBhmr;IoGJ@53<`*wEvn5TYCuz{Lc+r$2NvjS!SO!#$%c~ALwdV)DlPw**U z=r|w!eB;Twz!eaMU!VMY=!CPUEB_B%KHaCIzNN$I4h;2e>>cdPtW7Yhv@`! zGl-qT2}6wO#JT7mh}(ht_l>N~tiex44)(hooOd~avSa%9Vio5vZT<(ZxPS5GKbu#l z>gx|)X#y+cicwqtR`S8ge=0Bk9Yp8(CjmK;iU$x^Gl(gW2na?1fHDGqKhk%E+5t^u z4U~bCzNP(%ya4US{_mo=xPDoR{|?5S`X7HtgN(il)BzZ-ep}fy(k~C{)I$7iqyN8oP^TOHLlhMBVK67Cojt26 z#13X?YWdS9T0l;`CVfLg2nUbM%*{}X`*>yKZ9%|j3lr>w9+@j z;9)<*i-&eFeS24MX#_ALQ}7cYjz(riP$>+g1TO#Z%Kq0Hn`n&=gzbi5ebQeCJxngg^+0e>9e@eW99yja;7ZenHMg-b}$UQ#AP79 z8Qz+M?pbeJsu%INyt*jjmb5*p5*c4Gh#c5QRTnwsp*1Y>bY@-{_I-Hjy*J^uj$9or zN%~~i7wVN#IF(J5LXInyx>c_DHwEmB_$27w#Zr;>pl4df%3u_O z*%}4T4S{61?S$&S9 zIp0iL!kOMs@LFDP=^4IL6L3Z3?@f?-#UhbNlC$S8-+K?8tY(7YJ5ghK&EaNim-Vpf zl<&MZ>rGF%z;v?X)5WIwh{@{cj5pw2p||hmil!udF6M~4K-QkAlwoadYC1LDE;q7q z_3stl^NJlA8PN%2kk363!z09wc>JsUZ4!W@(#x%gBO1jwdvX!Hcn^_YR}T; zO$p)i=L|Ne-NDl#hCZ?}hlwR1ie8@$^J;fm(h_Wk`u5}7XnGVQ)LfTP2=lT~nEwXMwJ2j4p7 z@pCb{D;qT1z7*CUeq8s*$0m`UDju~KH&V59#Jlu+helz~Nn=J*-k!xKY`S@7^-{Zx z@!pniWSU8D5X2&#y=P93LlpC*}pRuYq6 zA^?_M*;KjoF^l@?Ic(w>*T3pf`;+Srjp-;~OVMlt!EzX{aLRibqIlz*@cx*0lZ=C} z&E?-@Q>FsE&a2#NSqi8Ws{_Z=W_oL@mF@7Y=+SqAINXe25oFUFD$$7?-6}y7v+uP& zYeR;?yGc~;8(+uvi9-cD1m-FEY^Nn^EO!v|Nlq!HQR)ZX!Ja=xv$L{Vhr1Prn3U2> z!Gb1r9ETxtG;-1k750nMH(h=ZLAPHs=WcNMc^vLJn8e%qvfWPMvm9dAYYgfV^~kUj z_q)8C;kLiF+RCQ>>5FWVh;ZHl4?NLzDKmcDcBWx=#Q$OXbl*yPz0QZ~d@f@o3-9Xy zLnmzFZObF^&Ql>d(GXX_2A7wYdw(bmdxSm@x2f2uy$G9rWUyE^p9oXRkaI|aAD4;d zsntY(=yMU)W6ICaXx1n&MnEvS$RSlS*pXPx)l<$d(JP`WoVu5pF+1qx>Px&U?k)uu zyR@9URZH=1Yh?lzpG_b^LTjb*P0BI1#C?B^ycPY(jOO`vy~N6qj<-FDpD3SAkgDMA zjUveEtANDb!g#&VUxQtYW=dsKk={ZWt|RUNzF(Y4Lp&FEgjOKKW~z zcB4d3YqrT3&t60q%ExIz5% z`&#Ape5X?*B`h$ktA}WWeKhuc`1Nqy{@hQRTSEmV@)ir z3`y>GVb?Ftrf7Cr6jO$3Q}0+UMAq-j!6muW93tMUM{* z(f8itWxH(8xqQudpBp=?q~m{Y^_rHJR@^9$BQQ9+{AL=<$@jg-MxiyU^zw--NdotN zgouxk>bZwriIJr8D9kfCj$Vj(7RO~01efb|gvx%lpn(Ajbv!Br3yzsY}G6Rwqb+_@|SW$iA;V&pG&)Cgv-uFS`E`jLLC%CmjZ3Yk+ z0iujyK`Wco;jhC5CWq^6Wk24=H-*3Ib7?ntVpP4|A*5^cQTR%XV4h>J;L?Zqrc0=9 znG}B+JT2R(11^0_$NuVplJ5kCCkNofl+WXB>EJS72 z0*^miS)?U7KZlZ%ar_GC0U%rG2`V9 z+g27XAN|`zJdoT>(9O&aX|}5HZH5kX&wJ^rXMHRmBCAI(Wv(RJwJ`Y~OBuD8^do?d zSNl}GdFe29sO6EO$lDe*C3OGYhJVQk9`U-(NgUd9yYcTMEuR!FhEVsVb0eYkuSv?@ zzb6h+J@zIL+2~aN7^KVg?u|X|6kGLy0XjP)v8wM*$NWdFn$719C0ZD{78+^Tsty5@tOH zEFlX?FEer8B`8rKmX^ypVz^9r_0xv+{GITit$%s!*5H{okcUbI|3_8GBi_&*{=DuD zZ?ns8zZ%N_+hw7)&P%=q8!;w{%?Z7hi8v{BQ4(p#Il~H7or;~7OI;28^z6IC60s{o zWu-c+w1>?MNe9S}%qa|`#0BmCZKZ({>BdsC8SdPnZru+_5pZek;}sK8SPim2FLo&C z&_^QG@k7zKqcFWKDXw(=(Uhie)4R%gojOkfDMo3ncly!v$?tprgL#Yv=8^0|61PAU zMDM1@&eMG=^d^jf|DXHlc?~9=>g~q&Ap8CT$e;LI1 zfdY246|J7A3t^FKTV#X1Fu&MlNm!{NiWh^dq3Ud;wRC+8bxCGby;K?b(D4+3)+xoIoLT3awW@2?`yeR>;Rm zbe_hOFs?U_`tsW`5aR`85U?EjWa%0_Nch1H`{FegF(XwMDr)!Nvho7q>vJ!nV9Z;4E`AO9bs$G;{4Jd{Z+k3HmIagbxuHK!=JS(h z$+(Q`LnUvzJn2jnmewbr06r}Rk$Qz=j@$mYlU}R zR<>Px=Il9pX7yM=BQbMa7OmoJTX0asO*#{+XnEwmiS z*GFN%<$O$VM%Wvp+)7+`mm1h=j)=Wt68om`{`jUCH3R2vLS#oV@bEq7I}#XH#>xsV zrU6HPhtPB|cRby3b)>2o_#9>$+B8{*t-y2BsR7Q>6~~pKuP8cUos06+QzIqNnoOYS z^Q(1v*&j08!?`Is5Q7pmYwlZCiLzow(P zOZi<7fFE8m>FIXSLJ1$6kdeV_Ij`nss741)7cV#vUYD+bHSxPVEDvg8A--DwL|%+$ z8~AavDkMy=;GMl8J_Eb@Suktp{21C|-1yUFuW`J`MiVyQ@RNbh$NBhYuw~2+Exm-{ zDn1Tjj9JrzW#k4t0fvf)!Oc#dF$&GRaYq1bZiT3;qpQuxm<{UuALeV#k0o@Jk>reC z8n_k0!kJgGCfEh2M@IWI661>II|&Ix&SAf5<2Fih@F;B;80-*ed|cxoU?}YMJhD~S zZMDd%cJ2+|gpEm(hogbkH(z|1>kaV?xrC9rsA5Z_&FT8JDQ^Nt5ZaAdw_9W4H4;wv z@pjU9p;_-(_2F9F!pK>IdsB6JWt=%=VFm}iNymMbL9dy5V@2qPuW{uwla>qw?RN;R zOBfaN8m))}h5SJQtmlHPCE>tkN7=)UvUOF@4-(P5fW?E_Fxj|&5xgZ-eKZ8pq-(m^ z)id6aAXMTnYPI<>gvVl_6htlptJpw_C+kY|)K~BE zVEEAtZ)=`AP+S|9bW;HyJdagdK)kE-s;zKb`2?*HdjuHH_4WgFVa@<>+p%MOAAeQh z1~!Kma{>$&52Fa{dTgZY(63_kK-S=i!SeCp`aTWUJ=(NFe}CRvuRDwUVL5%Tra41h zR`MI^i$CjsJLfbTp;qpFS+L1$(e7Rx^X6XD?O<9v3%U{~E@mp)p|?i`we{g<%ni!WismN;BntX>q4xM|x-~;*= zUYbv{%deD6Capbfp$u}nLT3M|Dg5ysEJ383(Ojk0HX?>g_6El4hAX*Sx zMCmwctQD-_1IxCLh=+C90;SXDU+6v$(c-Xpv>M3Ov9T+=;4@i|{qV(8kzC!XkwO1a zw`0USs!TSH*QKV2`(QC6sbd-5r0qaQYslMhAHvETfe*i~UTTwKlK!oQ3ItQuUtjz+ zL6tB#A8n#8?+_OtZ}r5eC8%mE-D!P7_Na(+tM|Ia9M7H=8nxd!oBv+mlFtEf3>W?? z4I0W|_xpJJTVC?8LN^`)M=P6Y5KF{&`1%`E(xxC7Ocj z>T3Kk8@aQd?W*RB2KThc5{0IopN&uC3A;`v>{_@li~YEjm928)0UM!#7BP>0!h?B} z_So6gSdWOow{on~MeDS&J2FX$8WB@n2yjqGOQ5UGz8%tEJ{^1=toPPrIFB_;if3!N0@+U{o`rr%^dao+D2pdt$Jvs zy=U!9yhI2i=RU2ZkKG6UAE_;}34;Bd$_=&)1uYAec4>RNmX?g1-}oU)kZY`(KLQ`N z4UO>derBf7JSSq*6rv3`4G4JB8B4=C`hwdE;}v|k2Sl}{)eJ^t_DHEfR#I@D+h@Bb z9)@ld*jJlK)zG<-QrCT)0bA438y&m(x*Z^5kJ>wm>OQY@r+L=;QTVi^1H9^H(ThcLis|l^m~BNnvwl zwyV?fHVRx7;#DFYQP#2TfyHGcdAfEet2ew zpn~)tHp@X|#+A&K0AmoCzYOa@b>^9*THi_MgkXHR8y#iy<`x#6JuAvul+dc(ND2{m zX9Fd}hQQ4iv?3=_cX2iP`0zM9;$lsbD7;!|F4{0wnRTubPoq5^fP=lGU7Tue?*kH| zUiHsCjzwLl_vbC{ZeRwU&qlR0x0G{cHg9#ezNhK@*ia+C>Wty$GIfd!9a+#oFwFL@h7ON} z=M!LTBqtuQkz9Eae|Z(tewq;2=hNdW9hI4zjnbQ-uduF0?TyN@1rHk2`BB^EgqPOVrX0S-%Hv-Wywt&7Bla%dPvoK-*e5#|V z^J{}O$3+zj?*fkAO8T?*N4@H{jBkm3CWu@}g?Yv$#qNI0$#Ia5WZ7)BP$>q!T6Mfc zP~@8@r5|(V99iG2)>rPh)V(6|M?K09$a1tRDy8N8w)aEtpq$(`VzS}s3-&;^v7!N- zG-uODIY&cu?ExQjVkJp7+#^6JNPbTgeFW26i%`>To@HO5YEKBB!TTC--y>B`;BoNX zk68a0#5emBqwtK`&)(P5|h$9ru0e=Xs^Z)vwh4Xs{-H(2cOc%j*WsORJN2$LR4}XBXX7vzE~eT z%DL39$*$C!zvM#5OJA|;`i zIA%nEG*lyK^p4d7pi7!WfY-SPdyIqe-(FO-jy&SyUEnp5Mrpj0c=w1)Omi)!v%2ne zo!7Y?^6cfVo+ti1!caNa`Pqd?FznxTE^QDLB5;8D%I^X`>Rhg_kBcV*; z06nhP#Qem@=4#bij40i>5qm@SHQNshf&3w5whoi)HFdeuAq2!zg5(qwugdN5SnJ(m%@~m&mVKvOAYqqtt75R3cB06Vi zyD0f4fUSC{aZ47)7*}kmhWfH)QaPK7^KLh+w9>t6`}06v+j@xJSV{Qur@Ud4*){`a z`p)-^sbs1W#!Qb5Kyde5Y?lbc&3YaF*Ric0iRH0(!(djnreE@69<`V9FmTFFEfn2w zT<8aw*Mg@GFpmF1Z@gqH+}c5`;51)-Xvy^Ejj`o>d~O9F9!<% zY-N)dVycslcQRygOhx7ff_SAkQ?5t8nYl*6Y{F4b(>(18P?|V){ME)ypUSre0o!Cz z{-cGIEoWx-WMS90ioO7Jq&q%2Dwz1?09#-uSLeO3*6;Vv-w40bRsXuJblj%*W9ek! zT+#a&X9;5q;)as|q=gPR5MyJ+WH>EyVnF)O*Fy-dpQrA+fJ3Xh+wp|e>L&X$G3LS$ zOGRw*&|%|JGh7T29%6`~EQogwdy$4ZG*d?f2)kY2!#n{COi*U`AYR_JaJ`_XK^^irgjJQpxQN_sA*qcUm`LaQqq%jABe9BW0+(v z+`XUNgZtQYt!Pl!8zh0ob0mXr%5|FB0fH^wN;Y91%+Z=x&C?w}0Jfcmy*G^f9wt8# zA_Z6@la8Np3lr%Ij#hgdA`^t15RPwugLwB^&1r}CA^{OqL?gGfop20PG z_2uq7C0vp-{L>SA2M~j73p%dwm%`D<;~*@VDR*3LS?l593DU_Pt`}UbP@mC*XlGFH zS=(o+=Ge7Hu|da*twyI8dovCtS+7$nPwQ!)WMBb6Kp127Ek<>V@cKU{qDkIe=vE|q z=X1}PFBojcO1r?geHtKUfnr8m89xzs%_ra}DcbcNHsBOW6a%tdWRQz59ZfgjecV2X zJ6}UySIZECh4McBy4$^V_${1Ci5!!0yNa^D{o+?GKq%r3x=kM(^XFpyfP<=5ORZGS z9v-OIUT4oI_5hD*OUV2D2}n7G_bWY3CqaJ$8$5x9^T!I7mX>(sqS;dd62Ja9LHfMx zfkzPw5^m!!n`9(KHZV_!T?6N`AiC(nW?6Bu0SwSm*B}}ZCP#ze zZPy%zGC}d@-aMC>1E}5QpE%EBvY+5e%;)&2%NS8%0b0tKNsDAIOX4Iyoh9g)pT$xU zYV5?P=;)K%w!D9mp;jH5)UG{wZzSfuQgOwxgdr!ON(wem8Ccj%x0BttI*rm1%-NQg=Gjcgu1L24XzRSV-nOzq$_$7;})l_%_qUvYS7K{p3~V;l>4c~iJUpeX0y-7HHKW_4Xa<9@dlUl>&(L&}%rq@#gZ*QXl}82F$2ECq4GdfGzJq z?dHLEy{m$52qCvM+#Y_5<*^t82u{4C-wKF?rBF~))5j!9e3S0I0hK_KD?k=6Lw38! zZuUth|C--`o+cMT{a))y2K|Q6z=>r$)_r}jFkw2>F1|y~w+#DPnj?oBYDgw4yPdZ=4M!%a;u~vRDbhrh6mg6My>A_=h z>_7ns&m-a{<7Sb-Z$L*g@}xH4wd0esa`;QTN#`KMVJ3+##<|D7_!LLUoW~-TUs_C` z(T8gI`@Qt>Z&Pe!T#L2s=A1V$(bf#hN4|6~2g*?gs9>#v%yCOCcz9EI*X!VcHt~F~ z;z$A?1Ga&h2eX>7lMjxIj76>Swj&Qx1qJQddb_pTe4J@#zXQ>&kb+N^#_-x-Rls{v=82PP+Xi*va~j`0M@^3jEUE_m~y^VI@-_OLzoKv zNwm&GBWb2`NoYAPr3R@hApHI16+4c)>X#mZB5I6#26b*s2GEdWuvmbArJzsz%Dj>yxlr=hd6( zD%6PRI)Om~KFg!9_1Cj*80ALD82QDlqbwra{t-bAcZuFV1DfGeEiM1 zR)1N48DVjawiK@gQ0s8i_8uqoobrf)p0u{rwdNMnPah$9#OmZ&=6I{1Bw=A$Bc?l5 z@^Jez41jv&7^Em^@R>{}&Elu|hr1fyT>afdB{^q4Ci*G5T+x++C_p4Ll!nVyl|)1j|$ zYp!H%z?!~nz1Q~FQam;qTw?G~J*Y&a56(B`)=y|#XD8L6o~-(Kuru)u$iYze{NZha zdqT%7ho4iap0#D9apoW39=i1qoUgB|W*8J3YMef{;JCLx1{29}t$sRa;*9@FddS2e z*OrQD?)YsTT>hY8KWYORmZwh@h$<;&eT2x0ci(DeDD%8AF9tD^l&n5fq;$$3%p zu6@;J;7S1b>30#HX*Yd>MY$N?8{Uq#>p)S>kLkGt8{e{zhx&ej3(i;X?jlxGhO%>V ztokk)a7eO@aEF953IY_y&O1A8-RKZd*M_{%+$KwW_iojWVunV&y4aFCe`%lZ-8*cy zZlJJ$q5RHaY_p}Dk*~BWG#ZR9h6hU?KpnhOuysrHrsZRLJwZr$wTW77F8mI#D>To>Z zE>vP%M;yH}gf8-gD6{RbEjFe-s4eS4q#4oLGWtR}2K9!{ua<3Y`1u`3CIKA-Eurd? zQEm`@u{|B8){`ew;#?pc z`X29=TU@-y(teXf5V%!yg?JBxZwgoP7m9n+w_`8FLX^AanP`O6Cd%q#AS%^ed6 z?c5Hu{h2Q|r?otdG05kvVZeB{JbbTdukv$mzn7IvxVEy9_P;Gi6n1k>8P$$~qXywt z!~w%P;QOj-*?pVaG3L#i>|G^>5ZmNVYXws^8Uf znI%C-j$amiD*#0@{kjxI#gxk(@(-XR1ul{=4s-ByG&;*yLEqZ(JvrOUPJ)u z{=JBBt?wTE*d2~fJNtF13H|+>pAd8054CCwbNNH39-ff(^2;bjdU*aM!hknX-zA_* z0Uf;jG0`(kvBqKzsdTyaI_>i?q=mB%fLv)dbqTrDZrpH`XX&H5SbrVXcjtnHaY~Kc zPo)g#o2-p9AT)nGx7^?q*W70*m!flnhrL&E*q!S^%|Siek(hh|j_cje#l-_~-U@)& zNaNS?Xc^NyJUf}T6|2$J7VWP&48jg6YecpI#wmqLR&b6?$#Pk0T7TFArG&+kcxs*0 zH|?5kRaZy$eOv_@=L1*bgxIRS&tmPmro}4W12$28Aj5k=SEw%>&^jIQr4PQjza`I= z|M9R<^q7MKJAjaW4hiefg7;+SBc)P~n&o#znUnhRSQ2fbI!Y7fkS>a7 zc9GkU0-I*#WU`a$2YqtAp`emSwv=J^zGG4AXMS^ET|{SzmQ^(psoTl4jATlO2gtV# z=W<;!lX8FEtf$R3>5*J^98*_As8<2!c0vumb>Fqv>~V;;Pw-onHd>k>oRFVB^l|<; zx$IU!;Iki=c5M29w>U0O4>@kpx~t~x_;ci92BLI6nYNu8gK-K1z6Wb8S+)3T~PQ6*!D>vGnd+ycuePj9K z$2O0tmnuapo-^DspxI7vn%JaKIv%DTI~PRHDGs0$)&>O@;iHXjx<=;DOn0Xb3`KeP zBcCh8lUpvK$#MA@=((HTpiWM9owxI5NYlNg52dcS_+&v^%nF>s@q6SJ;NNOUX7GChO&9rD>$NPk9`izd++E>w(G!o z!^~)u3bLh8Je9e+u8Z&M4imPZj(rReNkr+7x6()GZ--K2R2Xy@&L5W7K7PKIilWes zl_soa{ls+yes@}^*w=B$Y795Um^$H}hV&9Q1>>#EugdPa-s0_3LtHMzaAHsT%PP7M z?k^FHeJPyCT|IA!f}BqB?4>TTt~xK{R{OeRGldW0B0hU`_Vz3~<$c1(2Q#`uPui9W zB;t+tS5R@^lNI(fC?qrEelJ_ zo$33>%nh{YK*{THMK>vT?pJCo=C|XUl@oV7_`{gIKCJVcr8{g1Lq$`E8eXcC!>-+8 z2+dSik>o9RST3fZO5i&rJ`{W)vL%a9y34|M=eF%_(eNw6VPyOFXC*zJc*sLo64^!b zqh+VfW&>uS)#fo}kz7yeUcS3OPq)&)U)PBI9K8s07gmRl_b#(#!j~n9Ik@tvw!2Mc zEZ>zXz@M}Lm2DC&T9NqR*cKg#kPep;yEd)0fGikxTpnDSDKB}6_~aIEmnu?f-v4nd z*q}ID8xi`YuPB#uI-}E(poM`m2Tn2_8V+C84QP^?Nm6nDRdSp><;KZ#yLT+*YpA1j zUr-G1}BOu$1+nTQUPpGkNyKEu^R6c`a@Sw#d)5oURb!` z&bXG*~4gO1r65&sq)_nh27_b!f$a(vms?nw&rp3qvA2 z+p5RHi%q0@uuDb^*38W{X?xSI2s=_PPHTEj*$tmd$_;jA*>Ar3GbApU_&U@fda-op zW!Ptzsq~MO%_!%@)omb5ap;cG>E$ zG!TxyL4_ujpGN@$ruMmLI*C-PA7X`-iqrT zvRh<_-(fRGApMH;c?pD8*w@3B)Tx>nIA_LQRyPOj7O=x_h4HgVRc1r!5zjmhK~CJP z_8ZdJT_jM}rNnL^1H`8*CQYEwa=1?V?pm*Yih%*NnJ1ZddRf}`dT-hh`QRt$Y=s}K zPDP4Gmf9t>yZc2QxD?0B!LnR3H!ACc+sf+n&GWROWV%AP!ZvQIuS{aKhl5h5_YVb5 zd}0tpD~gNJa7|v)F^^|Sy-iHys+JHrhJT$sIWVz{8(tG5wJa(>|DB<_xYf z)*1gc_nJyIF=w}{FN}q_v$BTI#(DHP#UgqY72+1hZl_+w;O3wrBo+lq3J7oZpZJ9s z51i_Zzl@cFlBv_Mxj>EOFsD;1YsnAdt*Uj8&Wmm9?(;$}dK{ta{a4)e#A`;10z8$( zMNJn`N)&##I9vDkLnD8g3)5R*4cncfqWO-Mqx#yQ?EpV<`^JVB0m^%MCT@(XP@IFN zzz06|;a8cv7y^SdQi)W$U+%)7i=c=G_*Po4WCQ^$3a)^&6?2<9)MdhD9?}7&#H?Ro z@wC3|JwwdF>70ni0M;D(q#tpeoC5>-HF10Wb@3XgFG#rm%dJpm0!vE~_fs|7c?mi? z#Hg!quHDss)g%LT-S}0x@8u34S-(qU*wiOX0C>*BfNQrnl_IZF-FjS><`4O72 zvog%36Z9P)ywK|o{U^s&&HXhHro3#*o|Y3^`1yoaHt}^H^S)$IvB5?O0PvFRhUb%* zTX)`k`3z|#F9A%t8meIyLwED%c@c{s{E(Fu2SSrW{I$V6ss{y6?Bt>Ws^yATd5m0s zvpkrmH`dj~jvX2^aqm0XFXI07zX3MEgQEZg%QOGl zRUz>i)`Y7Z1xr~FVE%p^+H{9M7wZ9<}dEF>#1>C`pIAd0ak`E|fgc}K`d2C`et z3I-Tp|CGvrh2U4hn4Oao@aORVO#UBg{Lk6=Ka&(MdK%b2ceK%2{3ZVL40);2c7e2n z>f{L54OyT}(0>ehWPkYD4?1uZX|fn;2Sv9n}aU3X3v^G|JU zL)5paZg;@x9!-VGJU_mVoWOUUPVTef|Ab@$C8!NQJ$d&-~oQ2OMnIM=$^Y#gUVf zAIzxY;nAHt(P>Rre>b*ug!l4aJl(X!ukNkpxmB-KC?E^*HKkx%yIs7 zCfjLo=ET)5!JoTk0t5rKG6Tv`*`!&W%fs)(bOaY`Rt8!uKA6tyS) zYhGK#pf?Q^f#1y|Ej+FGX5=`7vN9~Q^iMH3ZM0Ir0q@+kU^_Li$!~E@BQDbvyybkt za&jy}NI(@frsPs%lT`GSNDqlhSBpmpV;+XSJ1w$KE(CSyeV5&T<_EC6p3o#G^u{!2 zrv(yUJXyMt59oozBn4ED>g7*u)Ki?b(wWN=#!u52aZb0)3Kj!um`8dvtx}3k(*cwSZ5i?eGHwT zj&0E#xga+vdxQinFk?V4$El7t&X<^WzZ>s}I#eM7GS}+g1`_YRRiGr-Q-RL|XtT;z!M7ac_@??;rE3$Dw%k88$Hr%_#P%U$c`GH$c+ zyXpEN&!o2UJ?bN2O=M!ta&84-%q5Rgy#t{J9Rn(AIx4>DPEbW}YSflz*ZuQ|`}yZY zzNc&h_r?(v$$uu*AwNi4>{@vdX=2u8Oj7{59SPJyW&H$=8XcvK5q&)~fHwQy&KD;( zTXygnhK4s@^D@9_InY_wR-Kzj~4qwunvI?DS%?#t{fz3N$s!@r?48L4>gf&Qr~>c z;)ei`?^V4LDGqv_(JiTW+v#!E=9^Wy3RLFJF0-K~oLUcp1W$E2^bzP=#edXea}RyA zeH>-aO4`f$$wn;0>y0OCRM+RPf&O>#ZMem6pD}Md$wMeR@_e{A{pxmnRqlJR$zxJa ziZqQF&@wz%7kf@U_Wi}ydMha`UC=36t%2Z&v|I%(9{e+xM8n7k9>irOg`Z{4`>OS} z2}scSswVCl!$VAyrqh+O*E~o1w~QbhuOjTI)4wCS8ijXd9RmFIILyu|`<td%!6g-1H= zrM{?7a#R>g?(TdG0`;!~d$-rZ^jR>uoUm%rNUOVk^v_oW2ZJ98hEsWD9EN&C+206I zg7LmRh=y+Rkn5IU>b0wE!k+h9zDFuVi=%`Jb3~~1#H%^jDh94Lg6`bNC^dBe3Q6t1 z-nh93s&tVmB(eDTKBhe$cbOV(vwd1<0hpSfQ`0+12s(q5GK7Ds!sjs1gERb&o01%B zQ4NkCSX3wP{X^mLF>0EvoMplw`=c$ErxjDmiZ0jt z;MpT%r0A}nYote`4c;dX6J^yR!@_KA=L9eAWTf*PPU zd6TQ;5$XhaqX&Y;7TnE`;#1wT#HWzIY5@Z6q20ptSM?AEefx+l@@+H)zPAtR*$%WD_Y>1Bgw62aJq{ElNqI8WyaAgvMJ=Wz7ibEK77R## z`bHs1z(mE?KmL~THNX>p&=_Cyt_eC48td_CKC)0kw)G45V|;>s0;iS)P{_-cy3T{u zb53%^M!ZoAS~VLLW+tEl`l>EQc0ZTdnD^{+U4nLk(*FjC1Ys5YiQvPsecq#a^smmgX7DmWM#vS6iejj5-Qup@%&(qYppZ_MjS( zkE-w0Augzzs0dz2jP(`IqQ*3{;D}&SqR+*Oh&E_?9q}!jt;1Ny zciA#0K)V`%*y~<6^7K8EeD~B_z@NZByyj?@%`-E>xWck5TO_UvQP1Z2y?QqXiZHoc zL@QSz(2^TDoaqUQhK{UX5nm-IG#idKZnwnBilJ(sYpwxc!DxV%)yGKqHIa+;dB{k#4d28`Ugc}? zMY|O}$|zl1Lzh4VSrUIWJGut73XMBbC&Ct5Obc^I7rMW$uw2~UTOCCSwASdz`y8nW zT%q35@)(*fIyU!=Z?l$L#%-x-I zcj2klr)(c5_~!Y{1;2H<5+^dLv+;1&j*k`%aO!eX_h z3$wi_X&Z?Wt@6I-<)G&`$|CyXbCuU*6x;219q zN{Np?-^W+r`O?-tVL{JTefNS6L5pM$ie^O=fy`XW)OJpw+q7B}A>Z>>H)^RyCX5HaE~BEL zLY0!}q08l4BG)3yYhtS0_XY4|Pz#Z?H%Ur#)b50>A2YyL{Jt7IwBxWXme7vXv8wcK zlKG}^mg>YSOdi4XPj(8AP{F*r29hu(&*wjrH<{Yx-H$oPCrHcAt$yKQdZJg~7p-Y# zN=m8h$InDSPDLP;Nr?Qpg-wQdO-o!;U(enCZmhNuRWihF3i50-vP|Ra-28*j<^22^ z@a}k!9^MYkJX2};1dH?|l!o(1-kGwc#d!R+6?a3!R4UUftGLXbsXN(u_X`jiF?Vd0 z6Fya{zbbMz#nZ3en?6py4|yxh$`+2_E=VDNm@DA9C7%sZ62L<&#mLOcZ<1`umle9V z`fW)1yWh(RGXA2LqdBXwaQ-ZQ5`uzOKM-!pNRnU1r{TfsdoJnCi?3N2wv6dNPm=EY zB6C(A^x?x}6S^%(Ej6;1hFsdq7h|OHUmK};GZmz$L$qqjmqqdLETk(mT5^Y?w=R6tPHe zGw6Ay{6hPz48qcp)prj0Fh1Z@cEVu>9LbO|2wh;LKMu+GU+leSP*m&IHL3_o6eOtx z0hJ(8Ns^fd^3w;}j{p1Rro%SBr#fs^hH|ghB z&ay8Iy7@fpyM1Bfrrn_TSAEXf-Z3xv&NTzB@|LYwmt;PUm%Y4iPTqpD6VJm&%1$&A zr#j$Fmw$pH7mkd%(S1YZkW0(QxXiBiSB0G7`8q~_9FA#|S_z~4S@`Mb%A{~gmj?y$ z%44;bc$+WZ4krNR{(@`b{a=r3^Dn(drR0W{-ceP8#!T|WB{SX|!eX&3^bvES`gN5| zpVxG+x7|Qj6jj;kNV*KLow^w&T39N!W+>UoFd|KdzR7=U@2Qn_UEF59S{s*980sQD z`W08%4yV%vuWvm7r4Kcuh3?4`#?7b7P-9*>5AgL7rmd-O(E`t0mRrea#zEG(QG@N- zIVa}94&LM+h69P45d{>bmcF%^nH(A2-wI7_u8kY1G7OX2OuGDY4L8xLEK&+?zZ@4O zdMHm*KU#84`V0pJJ-X=FvcMG}jg2U8FolYsPMZ5I>kIY&#GW3f4Q>8z);ye_5T|}W z`N`~5E#1U9Ud9o!U%Si$MceVBn#Fb(9CIqY%pGgx^i(2 z3=as)u&fy@#9hu;ubvU?dQP!}1I`0%!3YT@wxXy0fySv8#54i%14U#!=7Yv0MLT?*C%QY%u<{U5n}4D^_6 zr;A?;s+|8fk_I&Tvpl=MIB}_fTnZOKC_SHo1^wkG3i}(OlR6UwkgH$h0}s1ScW2QQ zT*_O3^xookExt2j``o{)3qf zr$^WNKO;P7-2c@>-PIK|0kUb43cwqNK{@bqHH>2)g#@>QRq_d74-C!pqsNUHk5RyDYlPDS$;M0Gobe0MO<1x0lLZ zUsK(G2=WB#mr#eX4LBvlUrUBO)T_s!@ATw(z`xpA`@5(}5TzixehEop(x zoz8W#0?*0**Ogdjp;2Yn#=5%??EVFI|8sfdO!;SVpv(Ku&Vlpk|NFJFJ}s#9ltIKA z0KI%@CU28~+GOhaX;~+teX!yG#eI=l3n%SC_y95l;?Kl=cQ!o$M`jwT^V@TTu9Fi4 z1XmFe5l66J++jJodMN+U!l9uf*L+yMS2(>fF13v|uJ!g91BNEOCgAdywsuQ8@dO09 zlq$rk2Aobj-g&MuNI8!;IzCdlBqGcIXN+{?;KCupTFPcHQg8zEm{6*T8W9Z-N2lMdXIU4CYfoF*N zTJNgikKd7|640jFZE}1!bmZGTmI`3&97F3#RIu6O&Pgr-I5zLpZcQwE%Fpg!lbWV| zL^P%;<^=b|e)POd1t?wJI-;>ceun$8|Mh851D|{I86SNO*#Od~xHuF^=eI<&pD)k` z3_4a*JTs&{N4mc^R8#>IpMT%*SL5z1!GL%Yx>@BL*IWFlejSo9#D~$c-h3wr&pn{M zJPM5_xxb&#@C66#3i$H$a^+cn6YhH~meiQdAZ{0ASKhkLUeC}!zzncQ<#On80kI$r; zgkW*0?hby4i1OeYGdLmeIWGV=_1oj-m;r0;Pr5*Nb31bv`Vp*x)5rIpf*`E<$~T?@ zUd1k95-#zW*4*AgfzjTcZj98Cc|_u3}?E+nONZnmzmD zomY79=aJhH(i1?TkrC(>*72yP)2|>CsA&(C%i(d#ZgVp$AX16{TC!{_*QTJ#+6s6$ zpLuP~4MxbroHb>(Qp*{)*}RJ0^c#fA@5un|{+?^L=m$cxQo>bxk?N-F4j2+9+y?uG zB#t1_iR~bYe<fsNL=C-a54%( z=r?LIr;8m7x)22wa+IEhEe|rmR|EmsXA#y=othhHSJi`3Z`7>y8v9NffLIkg4#Fd5 zrh14y*V+yqVHz!|e1tmAF4*z|B#w*W;uCQkYL<64|NL~^-Px+V*HX$>XQ9+&@kz9K zN|91}9Bi>BOPuCc9V?F>{_inTE)k)Qs)!^#Kh{=H?VlU7)4T$W*>d<(39o9ki4KQYfqG&kO z{t)Pnpx?F0?hVPxo@2}2oActcNkW_q^Y1~oqs511U~6}yZV}u4d_9x)Q@?dj2ruE-RMxhf9%)NBh++5CNCt^7vPew$PlHZ*_f0zIACxwg~t|PeqM(0nF8e_bL@H--WqvF(bvMOOh3^^YLGvZSs)1%~dvh$} zoqyB~-IJl_uIzu#PJx1R%1W4>Yvpk>yJ}1Y4)j}u=M%O2;;f3^`x$G){7YE_k&(<| zcW>m^dbc|p&U;rwcRSIbH0|B8{=7b~dm5$2=5)x%X0V0wRJBhfi`U=86G4Y@#J$^5RB^eMV1o%8OBGEhqS)bWZCe zv#XUJi`bO}?sXzcv6LpyEwg7bbqvhXoDc~2o&3~?+qS=Dj0Bu{H@nxuSrm4hwCJ0Iy*I2g z=83zg2|MNG+W3T`(BUSdMEH~RI0Rm@mI>YC)UWY}E}WDs-c;!5Nag(#G8drt;`UsP z-UlDe@ui(VYWWy?_0WEuH;8nQ7*h(HZdooCUG@DcP))Ai$K%=P{&eI!=-}25ks0lu zC2+Waix21XNA5IF$((AZ7mEMbs32q&Vt1M z7~}q}Q< z(tla*X;A)Ud#cqBYaslpvd_$mVgWpi%fVYvO#01Vl%Pg}|7p1woci+&e#`L&fIKhg zzct`=j3NQJ0}jj#{!W$TeU_KmO-+vbWs68TnrCSFJ*R)$E$-(;*DgVs`r6_@y?Jq& zUoPV4CLOqmXfg5G6aLFxhBEb>%HjO~{73lzMhU2bdxakD{k1~>TpphYBJ1$a;vk8~ z|Aw7&A)Z+JFD}47n-C7N|8?&`{G(>XnPLDE-dPa+_r_o(lwsM6?l3>Ln6dyYN5 zm{_VGhN#;m-tA66>go>J7(be&$D;8BRIj_W$?VKq82@i&S7g8I>?&h#?0aGIH;EKy z?3Q2I+BCv|0r@Z@0_F!}@Zp6Ec^xvMb*Jx6Cb^#$oF>P=5COP~G~O;hYeqj#SXj6M zv79!u0A!{kxFFG>7zLiOj)`6T!C=Aaa8HB7zcdx$G*+&!Vd1Gv<0?YorlluA@M`?N zGyx)>{vc!Vov*)g6w2)Tf0d-RT|!ZpPhhMV#QpZ&E*D~q9i19ZdD-i&hXUdv@w+9z z&AZQuVI{VHmW!nU=X7BBFn~xMvE_mQgs-}@HXKfd^ z($x82{h+oT=ylv$7YXAJVNu#-wgGs%__0vs=W57G1&V8GJ?b8z(YV;Y*v z2w{6z{o?RAjHJ%o0Y*$RnaLrvP^;LP4G%CW)lf6{-Ll*p7N1P)`n(PdjYTl8nHO~! z7ze+~a{{*ns^^L>MQ(X_$(gC@iEJ}GX>KovI=dS2hgqQ(0%P2z`TCG8P)RR}oIH_} zIH0|=tgkec(~n~l&cx53H{&_O#vn*KcJ1U6I1da` zjed+fIeCSx2p4hd>DG%g58FfFNf-cY?n`@XeJ{)6`|A|Tr3fB- zi|*Qp%i8WgXCoDO)nP75OmD#>PjI_gKCRQ1&e39FOO!a^*wsVC~ehmx(*yJ6`0rSxCCdgNa;ebgP=;x4>VCU@FyS<~cUT)EKky4Ge( zrw+_pC!M!S1^et$p0bl1Oa5llv&^F&3MHx3Pm$}EYza_e3PSS6xV-vm@R}3Ftf$-1m_1*8LVva4=DIIy$y;vv$tx_6Es{NbPR7 zpM??+!>`#Zx4LR`9woSt1-9Rtebh|<@eKn$1dlQzIG>PSk2fukpvapP`3jPxvnX%@ zeZbPFDyO0~(|*7RIyov}gXt2h%faHel|EVd{HO!%?O)tBH|`KzRF-Ry$;v&PODVmo z)w*$(|EA>D{h&1q{bu2&wD0r8P1b*ymbK9a!u7Smfy>}L?RB*Ne0!Lk7?&o)P(yA@ z?6uES0|n7$e8FCkwF6@MrT2yA2BJzBXmJxeGi*F3dbfB)Z!aQ0s4W=Zs(3WmWFlpB9>y zIfk*7B|W>=1P0e<1d~>H#Hr9dJAFz2(Q@n-_}$}u)d0e3tIeUa4m&ibaO5{}k?$|4 z+TUv)S_VCSzl{zF&GYOxPw%dp0y}L8VOD$&A83EIXsH-j55iHY@s*aHI9Wg6R)H4p z&oZDLFg6o=IFgWT%d^VHYd0yped38?P7ZoNM^n0Iu&3^m;H$PGoy;)Nx@Ro6&U{hk zjS~A@Q=dw8=y5=tQsS7{k(#X))WN%ijvabKhOyfWd?*aBHY*+(AP=7!jI{b-?^xwDyCESns=32n+- z7mxPt>8>M00-;rm#%t;I7XLw4>*RUQu!8D}Jr2`Y<&F4FnPWw|2|wR`^=@paW>{P@ zLfqoMG}8#-_Rot1gt2feXTtR8WHWj!w#k;YABtIy>YKJ&*=R8mRSMDiX(;sE@M`)t zR~z~()q*No((vFOd-U9>0}laE>wOkzQ?(Un4W!VpG%x0GGb>h#=M$07`H=teoVvu> zM{82)^9niZB7@xKBF49?mAUyD!*ZDWy&(2dJA8ZjSdA}=N!VJeRijbIZ9Id7YDG3o z_3d}o-KjSb>u$d`pNrX#)DAbKpe@JZ7hR<|lU$w)6ag+7}poyA2$CTfJ&U%K}a(){86lzD!0p#2I(` zD6E}rE=@JvLpZV$jK6XlGOtUsT#%AjnxQu_UOJF8i5x{ zDh(`buge{BCZVN4|4dPz6hP*5*gV=5kZ13Bv+?sc;}Co4MF4~9f;B^o0dV;3W?xPSAryoIiA4eRSiPb40N ziQSh!c+&7q>HU*j@AF<<&{nC5Hh!=`{g(P^k^Sd@=LxiR7^aQY#pX|4UDAp|J{lX_ zSt|9@zAsn2+h(PPrIKYdWlc>TRx413t^^6>Xo(Kp#%@oTM5MwYH5GJmaI?ZQ8O}yY zyQzW=*z1GXtU4o?&H{8bC@q9arC~||D>iZqv=ja9{F!+=k^c|($(xWWW-fi)IiI`B+Kyundw-mqt9Osn6c?$o9R7;6XKc* z-mOhC_(>P^MSQu!assh8HlyF3$tf6UI81)xMsI<6vWMSQM#>Srv4W*PNzWVZEPKGM zjh&zO`N(3`ljJruF=nc=IFc7^?IR}F$lIX9hzUDDK+LLTe~?jdcmp#e+nb;1yk(rc zGy7TlXEtcK&=;IIlO^7Eg$0m#D6=Gy+cGEpL0&FVTFmUMplnMomo){8%+Sl`>NQvA zk@#{A-KcpRpT2YrYq~$(M~*)nW`Fz@Q~Fe6oNj$bYihk9N@reEhPzqV_vh8!P%O#m62H|rHKT3M%NcHvwgG17PEmf?e>}U!2-uw zi+d)eN}sMMpQ5a_DjTa`(|)<|d@(hGY>|PI4@NS1Oc=C}^zZCoGq%_x7E`T6&G+ti z?G8Idx8Rq2-dJQNkkFJPXK#A#r&R66yG2VG`;NEVvdk+BiZl)dK2wTMV3Q0iX`U)* zZGWmcFsrxSbwn?pBG@$8r2HvnF{BxEJRyeWelWV=Emk{IpnT(AT^(jLJkoHki+nu- z_Y}ejL@jI&XC)p_Iy<|KGFoNkbbp>7B{PiHaj)0iygPf&eri&v#2eRfbB z4>lQTKAA}&ZZZ1?B~%>T9N2S&=svbLWADRdFNO574yQE9sH)5EMLliLGTUr!XD>3O z`?UXLQT*B`7jHh`G&=AnBDMrG6mn=mBx?4A*2vHWv?kU_wr_L0A@5G~{HKiZx2$KF zC1;AmS=QL5$Lqp7&7yo6TUm{PPx)@IcD$}^&8T+t0qFdFH>&Axo8>PqRtW}18s@*z ztivzUxf_b8vXd`v(SNBu*2tc6U3ERNQV_YIgXL^8#VRgIVYqe#pk@>wf zr4PxGXw}D)xZkGYs_~~lgK;ZqoutvF6dwW!C$l1p$L|Q+TX<(Lw5)@q?TnD>P;GBN zX}~7oDq9m2OwEX!L$e;~!i5$pN=8xMS`E62TP+MUf@EPT_lQ~j;dK~igJsbb|gs?#dl^WKSh(aDFkA&>u4kNqxUn`CKr+*6$# zI+g={TLJganji6OW3`xY;&=~3EszV-8oYnl41DRcrTydJ)5Ejm9ZWIVPw)Nb=jQ0z zhi%0wIi(uenT&X1G?9#5iZ~$^vXhpn3fb<#H}S~*wttz=XA18x-BLhFeY%{6yDq@B;v8OZc|U9mD-VKplJAj*dV0ZB_}^h?Z~y-NY9nvfu{0svMcxvBj}(U_!C)p$WB=Bu4ZskUO-SI} zbp^OnD!G(G0y=2EeZOvK*#E}3+;M2R_E&ZFEBa-6Oi2i515E+Xk0TPhJ~v=2OnO$9 z&GydLiWmKQvbvEX?$-|ln85FIYM4mh9evLR1hA%3{FJ3EhG!cI;};J@4D zW0~z{cDAhQhM?aSN|hz?kB>F74z?S= zHDd2{1_MJlb3iY7Wu(Ib_i1vhhY$K3xd&Qj6jZ`3V3xplQ0Srjn=8m#1w5x~3{qRu zifwg+DKc)*na-FWt}a*lx*i|ShJ1||FXW)4D+4*{bPLepx}6@|D^u_#;ja8hE9G)y zSfQ`V75pmUpD-a6@m2rGlkGRONbz54|0b!b53mWXA4~PcKO@{8qLO1}J*a?^3zR>% z|5Lc7^Ou_=2x|VW6p6vVPRMpcZq}cW7=4i9a2jsY{_BsoWK@E;S>$Jw{&p7x)T|bV zg-^VAfUF(KP&vdb4yqs6U@oU;ek@WeEk;1EsT%phjG0IDf-` zy1zIhD>L&v&`QVloJPe)UEgmXSUe1OgCKHexT^Gt zs#U0q+XqKn1o&o@@)`_^*Fb(9LJsZLT(&7G$B`#1GUq| za|stsSp@>XrY9j19ZSoDI}cd344^Q&Q#GCbu}sB|_!93Q7&wstTJp}RNlM@{(|c<| zXU5TY^$iOe&=!$RqjOXN;j|Ti`Vpg)nwgv5 zsonfV#wb|rHgBu2OY$7RZh*oozfAGRgX8)y*XCKVsj<>GBZ~_#r$>P!WB&l8m4}T9 zGrnqxt5LDULFZB;v(Rd$S59HnCJ=~^1f7xStP+=^8!Y#X5Mi1rpkOizA8rGAkioJ6 z8!oz>;GNQYn)~C=l;FWhxZ3tc0~<=tUK%%$muQjH0*AJLC}?gQbY`l z3=T!Fo${P|rN;R5>C-R4m&<+C)zv31l#iSV7yFp#Hei#Uoz3mKN6tk+d}0OkuT+BK zRQnI(0X&S-($dS{MXE2css(JVOb$k7EplhHSa45GSDTS~LdH^<)E@Q=OxW!Z^HE0D z5syjcH_4j!L4e+Jp@Hdk-xACA2TI=?gPU-V%^;;yhZB4rCk*!h?B709w-H#EPliLt zUVind#?Dwhiu+2wa{qPqFA^~MI3^g{(DoAlf*~0o#ERGp2They(&X|Zvu+@q# zTHB?;{|B{>nZ94jWBDvZX7)ZyQ*2n5j-Gk#CW zqRKw0y5ZTbmadS0s_(Ub_PPd8WxBCk7Tn}^(k&LtRli*6W@fWbxK3+y1U0WGvlg=B zW5PULAzRmQ&F3V-iZ7en)o4DcS{km{F?qHwB8A}Q@y&}OA}538iLPwa@EhxW2g~3` zl`sHJ(2O(+98Kb}+xm5w=#w(u71%{+mb4vKZh5p0!yJoi3DVc|lIc3r-jYUu{dIa) zZf?mgCO-qkCsGVN=KEfsURgAb-{&hBi(YD#`BBv*#8&(+!IX?eqjVom9U*j8=o~=AHdZI-4zDqP12^>_iGI6Vd}Uqi#Z{_#^uy}cg_4bi zM3EuWNp#Y4g`d>Q+)8V6E|*RpR>0htg3e!g?0B{hR8$dy19PXZkiFt(ymDs5|I>FKkQKU%D{Sf!|atafXi{$|!^l!1{)swhm z<2&FTRZ%^+j(^&PGV@^YrC@E-1qom@@e@{N|HTEkHhT&y&uV?p=YV1(x#uIUqqtwa z1Zz|?SJb3Z;^jgX@Qc(Mr4!rJid34_hC@%jOA`D(^9R)KRDw-b71QT%!A}l8Eu8UU zc=feq8jhWyuY1@aWwwO}S&J&RhjlK7ce?WYG zh++ZDaA4@vEyb3dUF3*237Ith;X|JsL%_ts#Iie#_VD?G`9mb}3;Q!GC4EmLVOjX&4t@v2z9CVbNRcY1i&HYus9`lh7Z5=?I>7!m^v{$LD9dT{znpl7D%Cy z+`~VU0{4|I;43qU*y?_Di`=hPGTy_0VG{Ij@zqrZ+>bQESG=yLM^{PihD${J#^XFngK^KCM}%hC&utr5<_BD)>_2^J&lm2`Zp?11~4x{)L@g>%z^CK@QQ;(b90p zebMCoLqoRnfivk8-Tz4bHo3v z@c+&&Ar~5HXliQeG4(2>cj3po?Giw5R*5q;_Fos)962b_%3;Li?-o#|q~`+=MZ&%R zUU0pHS6kzyl!rEyfl2@zmh=JLR1NG))GuES?k~T?vKMiv;b6g`A4l%95Mz9908ttR zuowz_@LC{uH{IEKl_19)H+q z6#x6B4@)0r12;l$y^{r*LyiaJ=6WU)#;L_*ZlTE5)aoUmgCsi#hIz|afX9a=g*?8D zQ&GAB0}W9yGTsiTv8`N5W*s#6um6iGGczlzU)1v3i&L1Re}%#{)|5r-RxqRY^6`=0 ztBC&S-+2Ugf0=~bUm{rQ?tq8AHl6xge0UhAuJ|2e-28xuY%?q0L`!P_>`1#<9%S`nXSckyxlBq$!oVyoAdWwa7E0$@JI zF~O$Q@rRDMrKQz}qZF{;*e3cD(G&PE+Nk*cp_PILBJi62`(v|5M*~PvGJ?m3n z4HU5A(DO&2S$E2J^XAGJ%bwVx=-$qxY(h>!?ZDLbRZ#QT{um4{`vqzfOW?janBQ6m zF##CUVI@Nr`*odt@U>HZPLAEuDpHZ9*Gd+T>`Yulfd=Chg^l*CVXb$i2NdUFzP+yj z`MpvQg?JuKbgBxJy0Rw=*uI^sVI=?w3?*AJ>vQ4#B^pFZ1rTv@u#ep(TOFASDt?iI58C9oNce)-wfD&Eixmm8Z%dn?b_wZ7 zd!fYSbSbotQC8&I(8dRa?GYeH1vQr?(G-f}SC%r@-?vMZ-LPk)`&a?`=sOk^kMd@ zgZj%`h~}cTFpc@R&AWjr*PP0Qs@XZ-^atlutNg!QioM2d(c9hqUiUS@#)zLRTk)la z7)RVT4&sC2MMK5#3C|x`ShU~XhF{xB4i{%BD4!*BJiwCB>WME>T2q04aP}6s=Abw< zU)o=J%3vw*ZWuIIrUh@`zP&u<;vfyqhwEE#>q7{G;N^XDn1+1igZ{w`NChWAHE{8` zsmrq@2Op?_?=6@M-5aR;s5h`nm+PE2Mi8aoPu4V`;d9S{TP^20U%o6N&PpGdDUPx5 zcb^}E320>wTXww$gH_3^(<60tXdx?&&{57PsL%(~U%G$!y0-I1c^fml=xb%w+$nQb zDrQ^t@tapQ-<6P}bP z6p>hP2Gmm=v*f7{UesEpE^=XB-GmfW)M944loV~N8cOgA)!;(y3xkWS2HfVkv}RHV zv3X6-u334f?hRJdnMU$_-Zk|z&e3MO_wB{GGI}EU@~5?m8MW2%JrB3d-r9P>@MIxu z@4cB2Z?H;DLbJO$*sQk)H#0g`uyt)%43fIoDhY)v>jYD77z}8q-hIPHlDzIQ&SOhPA zi-C4gjF$YXmePcw2;+veF4={sh)SnctDT{@*{V%R>dqq!N31vxG4m{JY8INL9pbr9<>SZ zQHeGb;Ilyd{tWxJ0LN+pxuC7$(`u={c|RB#gpQmJ;g+>HHMMI><(PM6P4B}ZRM#c) z?Tc^7415i`c`XFRDk9N&1^gDZU>1EPHXU=y_t7Q}kkObeXZg|@e!52*Og*m)m|kzx zF(00bAJ8^fneDiptyAof7pzu#4}!+v1((nfS?P&t4m*v*e@aA$+-0##QGL)hYXSc& zK_B(Q7Yq^P1MQm@yk%ja!&v!6ppvC0aW3ReEWIFha(HPvoYHEPB@p`P{flwucB_f$ zHVLXE{3E|!2*3yL1^~IT(A+VFgf1UvN7nQYD!saP@o`5BFKrd{vZMaABwS=1{;bT| zh^a-_e(EnUuTyJqaV}UZdB^|Eb=DoMZ}Z0n76!`;DBN~2{Ez5u_ek|BZFSXWB5XVC zb9ar4?S1R8o~>eIVY&$=zV-tpU(K#(OFrd}{Aw(JdHe3E0gc$zndg#xyO^Ur<_+a; zZ8^#D!(Dc~Tc(5dN{ItZLMXL~X^=@89pbB;>6S@EV}8@G^?C5HR{5s&B<5+>qN{oP zwrpf~WlXBSmhFsni_c!*%a7P%3i-NZH+#a)&y%_0wzN$(jG^)pf7`p%&@vKHooLKL z%4b&d{ge{#O_HfnrABf)HSl1EGdo2J!u#qX%4n*3yW>Z|aGrLtM0^V}|7?z=0QG0J zoDALCmsDf-a0UjK7LE*dyMGFo!Av!qDkEVStB~HSpI~bRf@e3Ds-U|td+>|-9#Fc9 z=PNvqy6llkkhYR`l1ApUkb08Q^A@hz`SG7WSCT0U^Wj-^%+W?8!D^-|CWeOSfjfPa zEzZ+L*NB|f>Vq$iC}R7VudYce_DmKm_T~IZZ&JkiZnn)@361x^tgf&%5xG4v(59Dx z;pCjx#5%iX*0mb{Z1nj)Dz(#RGf*+Rg$&O24bd5~(*2|2Ikh$Jl(Dqx1FOaC&Tyx+ zAQHXk6vrgHSD}4Cqt^@OI-t|jIRN{rPAD@|;Ag(HRLfORk)!(nNcl4BiJ-%@z4xc1qLB}0>|8s%IY^YY>z&CQ+qqJ@J-4R z7{re3))9OL{xSMw`NM~YC55tR{DRevbL~W#CSBr5ogy?GdFL{_&8EZogA|SFC!g1! zxAl>=pue>zA+Dd&LsG}mia4pNJ@w3*BCM^d8fa~tsS^pE-Wc4te%0;Z5GC3Mt97-b zwkI8xQesX-wShxzrZrABs>Svx7}h|xEbT(l^V zqgoEZ?Wb$2e`}yj`nI;KqY?t=Uo{;P*?vW17F<2PD_opPx=g)A@SL2j_SJTh%}6#yL0Gv3N9K!B=*8W=HS@h5 za+oMkzq=NK$!ucQ8)J_(xpgRxzO(>a=^CGqFt1$Q%X0q4d*3TY=Ecr48ND6uf^kF%zw}=a@r!Js*nd$er`ycYtsqR7QhEjO0kd9hKsM=nYv%uoCU$#*a(#Md{9k+J?qspj(SdmsBOMM~QGW ze1DqeK9f-@#35oF_YiB>~K2=X~&`4lQJw}bqr5eT?Ug!Jt4O=kODh!>dh`pt-UCOT3aSS;Pi z3tCh?xmmS`vRDU>VWr#0I@V_E_O|PUP#_Ymrii=H?U#bJ+AcJ23v1gQd$S>?FmdoD zd!ab~^X7s4qvO7*X8hd!lUn|#6~0B8P)*f?;gsQ&dkfh$(9PW>fcNrGw~pp9!9k~6 z?>X>wF;gb2#qBR);_YtVPvNt)6he9dGqpx?+e=*d)7b>xTPA5wm&(;xUp&(I*uU1j z^>>k_;CB~Xfy&B-V8c%t=ybJd@h~einqUOfi%5@@NA~-*wj^98H#^(5A$`9YC09WJ zJ-Q%p&JOv8D`QoVrWZ_sin)7yngJK9#YtfXc37lg_#w??Twm-7|KEV$7>o|hj_@ZL z6YkCF!)8Znj&44VtoU&RxHC5K@$qGC$+C4;9eZhMFvkPQ*1xHx+=8xF&XJ=`VyR#d zW!dXmg-0c_3BV9q5Ns$v#KR!18xJYsEsf1z}HAiug>g~nD_{I z^z-e1yuGcYr>Ll?V_bII6#qF?Vs`^bMYMHviZj93RDUp0-N@3eFTI5~&{y~Ur`gTBLPQ~)<&nd~?W zR#0bYNa=>K_0q@$nFIi&ISVF??F0%A8;}G%BEoZ}K94C)<34@~!S8`9RX=!$TQ-w` z^jv&BVw5&-Z|U4$ViQFX>-*jtVe9OD2-a9_1(%xYEQ0Y|!{TNV&cqusFs24!QVlc+ zR85^0a-GrLt_g2IAXJ&%Q1hLShrc7kG!Tk13aE*0j4iETkM}J&}((- z<|%L+qo3zys`jzF($@1PddMyQ95RhVNMvnsWCZAn4c7G9AG#a9oLfA zBduWG^3+%17NpNtWTVcu0R-2Ej2UgsuDa0fxf@q~`@^E2GmOS{)iOUIe5%yntaxaX zbA~PTKCXti3NYF<;}hB+tHUb6 zq`{|sG;M*}jVQL?e&86Ci2n*Bm>$fd6*|E3?S|qL77d^>-s;nq9N0j>`&8wqx!aEB z13!Eg4YqsTX|R!5^RybJ;Sbc87gOcgFma+L`9Cat`XBb@ElRTU4Ub^Zr#ILFc#&ZM zxwliXY3N48NsEtH76A9=w`>`(OT~opNh26o_!c&m*E{mw3=A3Ajo3ChMCG#QX&#yW zlhNNuv?`5KK#d1p-NJ1MZP*ZiE7B8o5}JX&dV5$kxY{K46H0PxV^HgnPWwurwqWey)3h*K)E=>R=?QVD#`VFdi7RNGeQk3o2_M>4iOdU?v+Vz zK~?QeT{EbwwO2~Vq&GcrJy}ffF2>@ITDWqV&*oZA#8=9+|G=S@XyCGAqkL)5mg+%}P&P@d*wW!i9|Ax+#x{LYiY8 zQ=aiyaF0$ul&-pz2dO%G!bLfcKct(&gLbt$29>|VnnqRbv4VP&pk>n`fzZtkb_R>J zI8J*EcPoWHh}i9w0MwvJn-RE1m^;Il8s`_ogjyRb@08B>y{_vKkWP1_9U$#c>N_$r zw5C*?yA`D7XKb|tUTXazoEnMN%-&unOGU5n8^AQB-Har0jqu9FBG0v%Zr7c4*P6|- zCCpVwvhHLV=AJ$=3wue~*TY&{(qCaYc1kFT=&*)tc-3f-|(TN3TqvmSbn4E|Q z!_b4A+u&N$Vb2q<0ZtbByxaGiA(jibws532>zURPjLa((p=qPbeC2^RXo0821mP&j zh6t8Bd)M2w_&m*u{RcGM-?_3)q(*5Jxz0ER^YrDTT8DN`8oPgI0wV^EDM{jw)iVYt z=I7BgX* znAPRvzOTCwD;(a7fe)E+keX?Fc0U)E_wh^pQE_my6PgDa^>?%ykx1#Al+?(qk?igf z&_zC>IPdv`5}lm`SR~^2Jprxk;`!-2`WdJrrQs+rGEO5)^lj{RBaJ34Bt?I%8}Zxo zd4r)X=`qLF5ybIpyKZ-kd8%EnN)_(&MQjJWKCzWgU6IZnbia#TReZ5m;~4Z}_4=cz z0)_Me)$n8#L)XkkLuppl*N2{YlObv&mSDb#b``!oU8cXyACz-vce5gL`pd}E5ZTtP zno}aDNWXcGI5#NC3?94EBoH?9&@@O*!v(+Dr|!dQ3_~DMj^hcetCKv(<4y@!-vPRh z-jqb7Evq)`-+b<9eozn!UD(vN*1oE1-C_BchYW4oHvefj`uZ#v*SSm9fJVY9 znA#2u4cRM(^>*y;hz>1UVAxb+XQ~cNe0;2cUQfGxus2w(u5KoI;5ruJOKy-2=gX{V zA0-X3)sgoEBfgUbC79<8`bL@V6xLS^5n0dQ0Vot*IK%(6ch^;_QDb)Mnx&t&N#kn6i1|pctwZd@ zWF1QyGi8nWl5e!P^mHJT0;(a|pil{4DFY%Hc7%+epOE&vOI8lS!&>8Mg_jE9wN_b@ z(Y74gp}rF1J^JRQ7wxp=w9j{vBi@4!qu2b~-9_mHu?Ny2#16GhTQdi%VL4Li=T+Id zM(T1@1GKmG8LO7UWQ051kdW+<;Yyn+d??dUqAvuzChF=X^6A&~gsey(IVLEqUYM6e zx5~DNIFRecE)2}>nED}V%Q4Ifm;K?k zHCt#eqEVe!zf0S@bD8amySZxd6U&X5W5L?JFowjg3zfT=p0=9|mmq!TME7Bh&@9G+e3K(K9^HdwL>S51yOsD39 zOSDH52Q{}VJ;vZ(5fyE~f&c<{a_{FYe?OTYoi_qh#?Dp4O6RmnaFh{T^pM^9s%ifo znZGW0(dCz8-f#Ev-tT6MWXJi>TEzT9llkCa8e%iJkbPZ3Y>n~lqKi@^%Z}ljCA6cP zqnm#g*T2pN_Js(vA6ulvWU-FjEYFfOQj{Unx6^?PC)ru!X#;hFtOS#E` z7t@7aKOl)0T#eo!25Ame%#WNd;!JP?_)BTr7;EYl@uYFF*Lwx`684>Utt~By4pD4t0a+YZ`r|Jr)?g{-bYjS?4t^QL^7c(ili05P;2>3;!tClF{wDvyW zP{=BHd)(J6+?SY;auwQiajbke!jg-IaWyB2>|@gZ0wgE{o&Zzl8=N8+kwJs{r)G=y z6=omEa!o3WJQ~88^(ZJP)IS_o4Zxp=FXH5aF_4M>2QJ3@_Fy_98<49c$u+Gm^Jt3U zYnq*%ZHO@*nHIogPYIi>m_hv6@|Gj+& zoEE6h-##I1iz9V%83w2|>QcH++rwA4gASqB01!UBUf35*pTGf($|No`-8NnzsrxZh zY^BK@+ZfZ`-gy$Y2Z)J`f|`kEGc53*v;VdKQN8^OpZiCqw~tBvBZS1Y4&aafvn>%o z&sJPjRkhr%NSQYb)v=A&`rPy}t)zXCsND4c;H2GI>`ZG)7y&bBQ$X2ibn%gdRZ9w9 z?L;Y$<<}rM2|oez8w(B|IvfXt!1yOmNQ>``$n3Hl2|ol)0m?rzRIA(X!DhV~x_T|y zdO7hxp(yokO%%T!x}nMAg^%7THBccY3fpwP2G1e4&lOH%p2xUJdb}&%hw$L8v9kRh zoT3t7fN6y{h;s>BmSq}VZQyR_GIeGplGvMeMGxKp~q`;Zki8T^*p`w@#0^Bc|EImLj*4o1_ZI{e3{Xl1NeM}% zK}x`&kq~JEha5sm1cp!qL@AYS1f-=)LKFe%RLVgqDG})y_&s}u^S$TXKYwf8weDT( zoaH}fbY{=qv-kTx&*%As9#A4ZnvyO(`h42A9Zx2F8C(bK_Ezvh?gx2L{B_Cd6i&kF ze`L`_#tUem2uUxWQQcb#l`qL*yW)UYy1KdfaL0kik0_1Mh+HSrc?On#NJpN^9dic{ z#!)u0+qdwQML|gYZ*Aj>qz8NRA7Vs-1);e#<@zxMxph)GaoM+rJS;`!3UvfI+-Duw zRKYKm#M62+YT7%=`=zY zdxt|U#tcarTAtIzJkir7Xc7UAmf&H($5{0H$1P<*25RsC952Ir0kzI@cXGjlt(JoTg(-Yo@ZW@UZ0iksaqOVkOGPp8j# z{k(q-XZCv{(zNuX-V_G>$WXz7{hyRXW&}sTTQ>@Z>cg+kaH6Hv^=myy5vM|V)W$Jc zX~Gma1-W5O&KxF2(`b4}fvWGQ4a8EZa!^9NObNngZF~F_(GdPlUf8j^(u&U5Cvm%; zB=vX7g1V){EZ5DfGq`u{NJ`{}9cbskLh~LYKxe7}y?4pEz|u+-R+G|QPlP8pbAM~N z@SX~)3IT53jDQYuu*F%|dN4fOYmh@fG5e}e9r<_#bjyg1n^4I`rm38opHrvf0%wEj z3uf9^JPc}!I}kKBIFR;vn6-x2RMvY^)7m_vvGGr@MHWgKGjdF8h#^o1>iaX8hj`ku zrXt*(A}Sw!yOU-e}voCqaYW9i1Aj>epI)L3vZ+aBcgzcooI5!sk6m7&R+z z*-Dg|Cy57NNy8S z@VO(Hm6^UPMW{-ps|NeD+$&!j5;=jM2L)!4e5)DfUzXxPZfng5T*aZh2(w(^(ie8D z%nyB8y)$@M_Aexrn$l?RI@53^t>#m>9h-{@8*9*Gtzp9>Lx4x}G=7VWu*GxIu%IdRGceIBk zu%l&*E@{nXCP2!-&?#LUvZrmu@e}opWobvQaLjg&8JJ$;o_B_O4;qCIl8g_>F51-Y zz*6Hbk;nvuo0+OlKnu33-Opn##)#yu%~d{BkEGy<%l>85!P#hzN35H5- zeT88+rNw}Q)b+?dt5xk}L)S^?l?fsB&5F}*n`1-$W8o`HwOdx1wF?N_7svwbVsG$E zPSP`kPDFM$Gts8Aw%*^ukCwD-X2!}9{BTh9JzER4b|3tRgNJDbixW^6N5nyh57_8} z6D%_kD(_kO9-lL_sy4K(XlN(N;`1|@|Kiu$U#j<0bJT&Qr!P4)bM$p6lH&07ZC3O} zQd2q?XQelr$ph+Tva%n!bS7r7keedNNdBmqa@@USYNJ?lVl#WKQ22{c$3?xVST(GR!7;-U>KMK_b%KVIpX6eKGOX+wx~RJBLH z-vK{u&)q-%vfIh=$-SzH{ko&tP09~vwKPa2(5dIMR}&HX*Jccv>JSD2c7&~%$Ylpj zZfg?A+PT(B`>grm=ctP>8Xw7D&)b>adWOMOY(_3DW$F2-ifnx! zM(5k-)o_hh?5wX$J^Z%TbC75zn|CDbm0o)y5YG;TTmS~DMMj%vC0LE?MT34Eu8mn{kj^ByYJmAietCrfNxxF5i@z_;+_hxd zOfb9eS(5oSbCtT>=!`A>mxTJz!OkCpUA$?Sth0tyS-MOgE#k++=j;r2buhMp#A(NpdkvZt6;!^+r8n2x z1tiI?9dMx5a!YsflwFsVN*BlHFMQts!#$*wF)}B_o2rwB>al7tUY0^cA_+i*YFp=9 z=FS^Lq-9VY^cev$YtprLEBgp?fJV-# zhpy~rhJcY~-?L2Ae&eba)?#JOA zvtJ z=n(uWjql_2kx^JPd*o{u8Mn3iab>`Hem(gQ&Vy?C56+_@y+REK)j&S`G-Q_4sdM&&Mx0oOx4x_YW{ww`G<;+bIyz=$gy|-`hKu^+Zgd+vY zS0(4C?Sy>@@0!HnPedpyF9ScTC@U)qZ-{{Z2Mkg^pFUlYpL{=-c8>rs6v8=xZ_>2@ z0@A>B&Ik0N#|2fa6!%1$y#F*!TFp4P-b{qK?>*-Ig=k^_JBp7x|CGJn-8dL6NvQR? zXndXmp*xRJ+x_w6lh=4c9Bt+MS++?;~lHlmgvZAxKG| z!x(V3@4LIXKj9iNko?0jX_{GB;|1Ywm^+`!`6Xm$X6pB50A~w4nK>-(m*dY^kq;7M z1}7oWj~CoNnCP9aA~yz{tPe7q`&aZzxKs8n1Gri|Z|rObGY`W$R^{$gx?3|f{Pwep?qj@|7L zk?qA0ui=D|iQy{;?5bBHx;mta8)VEsN>0O0F3zCiy>S1ueSo%b7H_oZLN;5^f zmA2k<09sGMUuAmE=UiZnF~7J1@ncco{WT!x2CCJQxMF|#-bho7<@U;b+_=B>6sTU$ zgUixWbOvAW!AeiVwW)qDnEkaT?kscCKQYYKo8>boZmOZejdRL2S76*{-2iC-lNX>x zpK$%-b<6J@X2;tstv#62c@(D1d$>TT~-II^O)E4QbWbMMOmhiK@`z@QzUlvV$# z?_vHXs}!^`)~*)k;;xj-u#Ck-MflWLYdl*vGhQ>;_+&5zp6G=vjx)WPlkh_o z-!|Qyudfjj2UW55Jb&fS6xvx%|btfy#_nc-wdqTPq*HFP=lsiDsEC0_>hHR*DwpHTY(igby- zGkmrjrxtboz3q$aHlv}cDS_%`P^V;F67oW@CGvL_R_d#IXc-t|qks{g+x~TPE6l$} zJ6&IGrHvv4WXA=kB<`DiQuDm=M#nRTrQge1bRgPtXk2pe3xqDCht}a)Su@1iDPXBq zhI;%-ng_eBG~V~B@&An-!>$B7|Ctsf-Ih(Zbm`YHBmQdK@$qyoi^}cc} zq|{shEx~;D)nLC(l6;1IsA0Gd9*`@t=6ABHHsew~960y|+FeU^@$Zj&Z%wY{mdw*U z>9(>T%J9qf7^Y4~j8s^8Im%S54NH*tF~0gc2HvWxN_ht1-+L*ZVq{C~PR(e2D{7Z) zv7HS8%7(Q~lX`ICtiquG{$|nc)h?3`k5>JIM8;~Dn#TX3>214tlT!R9@j}g39zfly z>27V4=P0e*Af=-XD&^<4GHJ0L>p}9NUQ=TYL3FX;dNhEST7J%|N|~`EH9+T!u9OY^ z0&Y-6*K^rz&FodfA#lnbf>c#8?YrIVdRvykGgDwHiuN3!m6(2_RQVBVO%Z`yPGg^jzr}#j?SiIrywSF))R(twbF=89P0V$D&+W=FW zn%4&JT-1$Tei2byS8?6}ET-+IbO3qEq*@5M#+U*vB0Bj+Dzoz&{oWbVo_QBZgk->f62 z)ug(U>q5=F@=ks-kTaniizSz}$_Iv#OyXQ40xIp#2zJDIC>Kh5xsT*rd9Rdwb$(*0 z@>*%)aP8_E5TL%|c*fHuc=lycke7lG92JDi^7K>aOS6AdJMnsS(qDBdYv3lSm}jIm z&Us6vG&MByZH{+-B{;ptlYRBV&6*lMQ|W%+m+gA3Z?RE!r>mWLwpPS3ZZbktoje-7 z@+)9WIME>^__wS_&(Xp|(d=G~3I_=bGcXo_!cb(z-NF4c6@0=1xscIGOLIJFE950B86_DQWekD1I2n}FW5SaO}1k{&F%z~%yi z0M_<=QbGUhR-2!)^V%}Z9xA)xRXXA93oFJo)eLASvuallhnx9wCqQ z-#Lx7^+ki-H@ThC`|?-gs`#Jldd@tOeUTZh>-X?EQ@+?YJMs-oc9k5J;C`v4q_g;a zG8```7sW4+-xaWF+bj8AXnsg;r)>i|%-E{kTKUe6re0TG8mHwl`Io3)!YWgPo8VVA zzeYpzaQN<^|KVuzWO9L+z+TtWGzP;m)ZNSP`#=pu=9XsMjTd0;V}SN{;~2LAwEp(M{WQ8 zQe@y}(65&>9irU57A`6*S{)VMM_<{*)`xqV=K>v zGCk-sDV^8N_sz8BU4|;+>mOZz)C_gyA4}|t{`}8yX=Co}&bU(X{UZ#`i4x0Sbgv;c zWjSHAefgd9gHGtC&!l#>J3Sh*BdexDA-1xXEJY=C*()nOg0|OVF-P{o)LWH@~mdNv@ zLX(Xc6Fpu|hP(i?1_i{`?U2PMoyeYN>i^q z=e92r-t*COAdTQRR&L_`MF}h_1PKv@F&csssa9!};U8;d-K2touu%Q1M=G`WHO!Zm z;Er~QPRW6Q6%^~<0-`dwk)#yJ5Go{6%74;_r^jbxtnnD=`gsQhGY-I;5Xp zIl?Fvm83s+{5AoHJBZ|N{uf&0E_bxx!X83WtOCn8oiK&Oc<6E5*4jc}nVo6sf<+ zwMN1Yp_~N%XhRpBXO<^{N-$HdSo-=&1Od`hI$4u*gf|9YA2T?@H? z-QxD{(0^}<|G!(J_)s)r(eKar&|)5Vg&IS^$qVtGd5w3sBqAv!cY>&0ckqV&whgw( zGpA3Vj!7bjaS;-unfxU5dvwn8gU%m2lNH>Lo;+W-JU7szqoWfA3W3F4aeIc}lB1s9 zYd%TXgTMzuK^~1AOy2w6LEAe4p;D&|-+37f2oZk82^e5Zg06!W$*yPn^ql@nkfzd` z$q%G5M&FUoh{7I(`Rdnqz*3z6%SCpJh&730)dX@txSSt<7&!O4jseNRo!#1=^VERV z*v3NrA(?5s9VbY#y}0_f#z2k}Q)WA2(2!PiS+eR@8GcE>$ESJS(69>OW-EZ3l}H1WxjQHr^wo-btXT(M4~81qR4!m)1l1Si|(wAJO~q5wkKAvz{*| ziEHwJCeFYAa0x&DC1zqbjykL;Uu2t*PBazKaZ32(!*EDEta-$Q2LFFV$GO;vbGpO8 z-en&R72EK%JOv3lWz-cre^WY}Di&ayZ zJt%xWf;{kJc8j;f`dFfOz1F&6w<+j{&}$FRkywSkke#tLylPQ;!$43bVs~xMgsb)gxhOye$kvP6mKHXg zn;PpoP`kTRMHP)OfS^F2+sJRdRksVAg)87;YM;W!1pU)1hV`|v5ZNod1B@^L^TsKd z0r&k3|Bi}4wNstT)8`sqIEe?pZ#j+D`IxwQ$JhLMT|57=kFx`eV^iUCMJ0jV)BWc> z(<<&Ao4U&}PY`XTkWq2GM0-ueT4xTpLvM3dFv$EJiw1JT$&q{x@TS5$#-H*tdYCMz z&`|gm!3P2l*DgEfoV-?CTns;1`234#G!Do?!*gdAA)CITfpRc({Wq*Tl`iIOF@4#% z_wA&L>148sleZ$|OGTm_npQ_o+$^(=SDYMWjd0%k)dJW@6*(R`z8XIvCj}4tr@&TV z9i-_oA8BHOWyJ!ha<)$b@1)ExM{7HZCZ=u$jgOn4!zUzE8o`9Ea8vc$Q^Fc?0B&Ws2sQQ! z5=sgw_s2~}75DGu%bm0-diheXk6DwF5>T$Pt$~@c(o)H;&f#H(=ck0yBaa?fmD=A} zXn0^0g}|fF@+u}{;K@-;SiGEbzg>E5m@3zx+Gu2={c-_;}b1uieRH{I|gB=s}VfzA8!G%zUMTsYivCPX$A*(o*5`K_Wkx$FK_tY zivUwa=hM&1FL`ktlVHiA`=ZWu?1y5y0AnkEi;upRwUDZV{Do&>`Z|h}NRnj3>_Kr# z8P5yC?a_)TBaa=meNmd*^aqNsdf0@&wB(>F9T^!Z5IZdxxKf_e5y2#cY7MB)eDBsF zyX&EBc1-x{+q<6TDsMgvKc$N9-))U+9C12z4a2dP#oWPCms5!ne0BJ*RqFwu@7to1gUu=%5L6lo#X;;1YHHadQ7}Rp&DF(4_hb`fCOF0bZYR+eKeVoD$oi?S}bhZ$M~p>^km z6d3mhm~S9Ax{lPxcpfGyE(fExh;)}uKq#*!(P41|_vkmrF-6JU9HuD0yMaPQ6(8v) z#TFN(IIt6{Rfo)5sIb_YKdGR#eWgLmxkGsn_hdKqYKY4&{lfX{Z>F#Tza>snRE50- zdra34??Yr;*=l#)(gFi-VQbIt82u@V%VXOZ7OA{hz8D|c#JgQ5&qsKkk@fUY(4AA% zO(8qA%6{*Kq=hN0Cgx(SG`kfY&% zjy0CeZ}qC#S(pEA^x&lBS0e0g-s6y1yNl0OB^QoeHol!g?iwV$YcWr zwiB4;VSq9l@p-V#`>ko=L||a6&0=H2sZ}aZbBP~~$2ZS3Ismr%OTzeZ#>COZjYWUA z@ikdDn#e?2S=JF-U$YhsN{!&m4v)qEG)Mlsy4*-(jP-?7$3+)(v+qd>Fn} z^VRL`kx+e!$84-UYjhV-jZqV9j0YAu=1zXpZaqN!gh;$a2(8&+L)q|$McW;Y>G;Uq zV>Av{zAt3bsO$RtEgQ+=a+*I5JFA-gU@D5|5Imk8=T9l*)h3%j@ml&ZMUh8;Y5ZOE z*3Od96kF?Y!PadDeh*6jir;}nc8hFTsvc~Zw?`@FsIHz&Y>=0>W@5^%U5{R+Oh%;g zMKqs2IINq1o9C$|ny`Ds-L@p-n{g#c!Oz>THkZW|{NsjlkCW5T4$Y zy~0QlR*$6=SUmz{5*!rG_<;k$M`^OZ-a9T@F5UP5xSeWN-D;OTSk`S$^bBBuPcJ19q@YwYH2^r}6%@E9X-@|L;nX%1)G4*lOV&RJ! z5MsxBek7A&GkG&shJ%#Mj5|UHFje0x2G(z}@ry16iSwZbZ|`TY9j}w*m$^%(;nA^t z(dBt8mZ`X* z>xOKe-Tmf{aTA(x36h)VlA~28eD}_jIsu-A)~GjUBfaUA?k(@zw|`Da@siHbq&4V7 zcmg03{&k>Kc3o23a}*-kTkBe$?itzljf6V?VjxeA^nu-0@(b%3kJ0x|e-!Fz5Y#0J zvwIY;bS~h?QiDfb!~)gDVhlKds@rHr%1a%p&@_t~UFldG6QOnCFWvm{VX^${k9Hwh z2^6c)vyPMJ9iJ^wC$>N$_lTBB!V5D|G3xO3&xLXk)v*D=vsFfyQT&VA2Uj87+F9*8 z+rkt=KiBr8{}g5&{CMLT`fJ@$8T;kg-$&*kf30UzHV-|sA$I$3=gkt9X@nHZNWEN7 zWzQ-UhvR-nZ*`qSF$ttwl8WLfB?2txp28lo%USoFEKv2{NC3wCFi8K{)s6`6(MkH_$*8{bqB$^Xiw!YYLQaA4rC+I|jM+v6WOun!xH z=A@2IS9LtqkR+P$|MLMGuZyMdl=UF?E0=jYRG^4j_3^g)>jAHU2OR7kzDy0}&HdGo zgr2alvx{tE=w{R$BkYwE%5{OQmxE|uS#Liz#25HUieyesPS?yLEU?_Xdv!7g)X4x$ z1`VmWaqcxCxU+A8Y#Z2$5!C*xcx*Zdb&|EU_0nw>(Z4n(i$hK>F3M)KYIaS>2_JT! zbyN`rF*gzoIX4MS)o1TIfY_n$Ie^QdB)s%Ar0y+vqwQo=RzBLU~ z;oyZV_TXFa6Kc=lTHvdBy;AEW*V%j9)5mphX?SiRn|mRknlsC+i%Pb8SF=v(_=AP& zwO}x;uq#2M(Z-zju5s?Qe%^9zSPfNS!aAAYp^smcf1bv&O{L)Kp^@`HM!avK Date: Tue, 25 Oct 2022 22:10:58 +0200 Subject: [PATCH 05/47] Ergebnisse aus Architekur-Mtg vom 25.10.2022 --- docu/RoadMap_2022-2023.md | 201 ++++++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 83 deletions(-) diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md index 26a4ec41e..be3e197bd 100644 --- a/docu/RoadMap_2022-2023.md +++ b/docu/RoadMap_2022-2023.md @@ -13,49 +13,47 @@ - Konzept fertig - Änderungen in Register- und Login-Prozess -3. Passwort-Verschlüsselung +3. Passwort-Verschlüsselung: Refactoring - - Konzept fertig - - Unabhängigkeit von Email erzeugen - - Änderung der User-Email ermöglichen - - Versionierung der verwendeten Verschlüsselungslogik notwendig -4. Contribution-Categories + - Konzept aufteilen in Ausbaustufen + - Altlasten entsorgen + - Versionierung/Typisierung der verwendeten Verschlüsselungslogik notwendig + - DB-Migration auf encryptionType=EMAIL +4. Passwort-Verschlüsselung: Login mit impliziter Neuverschlüsselung + + * Logik der Passwortverschlüsselung auf GradidoID einführen + * bei Login mit encryptionType=Email oder OneTime triggern einer Neuverschlüsselung per GradidoID + * Unabhängigkeit von Email erzeugen + * Änderung der User-Email ermöglichen +5. Contribution-Categories - Bewertung und Kategorisierung von Schöpfungen: Was hat Wer für Wen geleistet? - Regeln auf Categories ermöglichen - Konzept in Arbeit -5. Statistics / Analysen -6. Subgruppierung / Subcommunities +6. Statistics / Analysen +7. Contribution-Link editieren +8. User-Tagging - - **einfacher Ansatz:** innerhalb der existierenden Community gibt es Untergruppierungen, sprich SubCommunities + - Eine UserTag dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User + - Motivation des User-Taggings: bilden kleinerer lokaler User-Gruppen und jeder kennt jeden + - Einführung einer UserTaggings-Tabelle und eine User-UserTaggings-Zuordnungs-Tabelle + - Ein Moderator kann im AdminInterface die Liste der UserTags pflegen - - Einführung eine Community-Tabelle - - In der Community-Tabelle gibt es zunächst eine Haupt-Community, die mehrere Sub-Communities haben kann - - ein User ist in der Haupt-Community unique, kann aber in mehreren SubCommunities sein - - Eine SubCommunity dient zur einfachen Gruppierung gleichgesinnter oder örtlich gebundener User - - Eine SubCommunity hat eigene Moderatoren - - Motivation einer SubCommunity: kleine lokale Gruppen und jeder kennt jeden - - **ToDos**: - - DB-Migration für Community-Tabelle, User-SubCommunity-Zuordnungen, UserRights-Tabelle - - Berechtigungen für SubCommunities - - Register- und Login-Prozess für SubCommunity-Anmeldung anpassen - - Auswahl-Box einer SubCommunity - - createUser mit Zuordnung zur ausgewählten SubCommunity - - Schöpfungsprozess auf angemeldete SubCommunity anpassen - - "Beitrag einreichen"-Dialog auf angemeldete SubCommunity anpassen - - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete SubCommunity anpassen - - "Gemeinschaft"-Dialog auf angemeldete SubCommunity anpassen - - "Mein Profil"-Dialog auf SubCommunities anpassen - - Umzug-Service in andere SubCommunity - - Löschen der Mitgliedschaft zu angemeldeter SubCommunity (Deaktivierung der Zuordnung "User-SubCommunity") - - "Senden"-Dialog mit SubCommunity-Auswahl - - "Transaktion"-Dialog mit Filter auf angemeldeter SubCommunity - - AdminInterface auf angemeldete SubCommunity anpassen - - "Übersicht"-Dialog mit Filter auf angemeldete SubCommunity - - "Nutzersuche"-Dialog mit Filter auf angemeldete SubCommunity - - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete SubComunity - - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete SubCommunity anpassen -7. User-Beziehungen und Favoritenverwaltung + - neues TAG anlegen + - vorhandenes TAG umbenennen + - ein TAG löschen, sofern kein User mehr diesem TAG zugeordnet ist + - Will ein User ein TAG zugeordnet werden, so kann dies nur ein Moderator im AdminInterface tun + - Ein Moderator kann im AdminInterface + + - ein TAG einem User zuordnen + - ein TAG von einem User entfernen + - wichtige UseCases: + + - Zuordnung eines Users zu einem TAG durch einen Moderator + - TAG spezifische Schöpfung + - User muss für seinen Beitrag ein TAG auswählen können, dem er zuvor zugeordnet wurde + - TAG-Moderator kann den Beitrag bestätigen, weil er den User mit dem TAG (persönlich) kennt +9. User-Beziehungen und Favoritenverwaltung - User-User-Zuordnung - aus Tx-Liste die aktuellen Favoriten ermitteln @@ -65,67 +63,104 @@ - Gruppierung - Community-übergreifend - User-Beziehungen -8. technische Ablösung der Email und Ersatz durch GradidoID +10. technische Ablösung der Email und Ersatz durch GradidoID - * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird - * Email soll aber im Aussen für User optional noch verwendbar bleiben - * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird -9. Zeitzone + * APIs / Links / etc mit Email anpassen, so dass keine Email mehr verwendet wird + * Email soll aber im Aussen für User optional noch verwendbar bleiben + * Intern erfolgt aber auf jedenfall ein Mapping auf GradidoID egal ob per Email oder Alias angefragt wird +11. Zeitzone - - User sieht immer seine Locale-Zeit und Monate - - Admin sieht immer UTC-Zeit und Monate - - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) - - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** - - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja - - Beispiel: User in Tokyo Locale mit Offest +09:00 + - User sieht immer seine Locale-Zeit und Monate + - Admin sieht immer UTC-Zeit und Monate + - wichtiges Kriterium für Schöpfung ist das TargetDate ( heißt in DB contributionDate) + - Berechnung der möglichen Schöpfungen muss somit auf dem TargetDate der Schöpfung ermittelt werden! **(Ist-Zustand)** + - Kann es vorkommen, dass das TargetDate der Contribution vor dem CreationDate der TX liegt? Ja + - Beispiel: User in Tokyo Locale mit Offest +09:00 - - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung - - die Contribution wird gespeichert mit + - aktiviert Contribution-Link mit Locale: 01.11.2022 07:00:00+09:00 = TargetDate = Zieldatum der Schöpfung + - die Contribution wird gespeichert mit - - creationDate=31.10.2022 22:00:00 UTC - - contributionDate=01.11.2022 07:00:00 - - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 - - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit + - creationDate=31.10.2022 22:00:00 UTC + - contributionDate=01.11.2022 07:00:00 + - (neu) clientRequestTime=01.11.2022 07:00:00+09:00 + - durch automatische Bestätigung und sofortiger Transaktion wird die TX gespeichert mit - - creationDate=31.10.2022 22:00:00 UTC - - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** - - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern - - Beispiel für täglichen Contribution-Link während des Monats: + - creationDate=31.10.2022 22:00:00 UTC + - **zwingende Prüfung aller Requeste: auf -12h <= ClientRequestTime <= +12h** - - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 - - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! - - Beispiel für täglichen Contribution-Link am Monatswechsel: + - Prüfung auf Sommerzeiten und exotische Länder beachten + - + - zur Analyse und Problemverfolgung von Contributions immer original ClientRequestTime mit Offset in DB speichern + - Beispiel für täglichen Contribution-Link während des Monats: - - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 - - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! -10. Layout -11. Manuelle User-Registrierung für Admin + - 17.10.2022 22:00 +09:00 => 17.10.2022 UTC: 17.10.2022 13:00 UTC => 17.10.2022 + - 18.10.2022 02:00 +09:00 => 18.10.2022 UTC: 17.10.2022 17:00 UTC => 17.10.2022 !!!! darf nicht weil gleicher Tag !!! + - Beispiel für täglichen Contribution-Link am Monatswechsel: + + - 31.10.2022 22:00 +09:00 => 31.10.2022 UTC: 31.10.2022 15:00 UTC => 31.10.2022 + - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! +12. Layout +13. Lastschriften-Link +14. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich + + 1. speichern des Links zusammen mit OptIn-Code + 2. +15. Manuelle User-Registrierung für Admin - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein -12. Dezentralisierung / Federation +16. Dezentralisierung / Federation - Hyperswarm + + - funktioniert schon im Prototyp + - alle Instanzen finden sich gegenseitig + - ToDo: + - Infos aus HyperSwarm in der Community speichern + - Prüfung ob neue mir noch unbekannte Community hinzugekommen ist? + - Triggern der Authentifizierungs- und Autorisierungs-Handshake für neue Community - Authentifizierungs- und Autorisierungs-Handshake - Inter-Community-Communication + - **ToDos**: + + - DB-Migration für Community-Tabelle, User-Community-Zuordnungen, UserRights-Tabelle + - Berechtigungen für Communities + - Register- und Login-Prozess für Community-Anmeldung anpassen + + - Auswahl-Box einer Community + - createUser mit Zuordnung zur ausgewählten Community + - Schöpfungsprozess auf angemeldete Community anpassen + + - "Beitrag einreichen"-Dialog auf angemeldete Community anpassen + - "meine Beiträge zum Gemeinwohl" mit Filter auf angemeldete Community anpassen + - "Gemeinschaft"-Dialog auf angemeldete Community anpassen + - "Mein Profil"-Dialog auf Communities anpassen + + - Umzug-Service in andere Community + - Löschen der Mitgliedschaft zu angemeldeter Community (Deaktivierung der Zuordnung "User-Community") + - "Senden"-Dialog mit Community-Auswahl + - "Transaktion"-Dialog mit Filter auf angemeldeter Community + - AdminInterface auf angemeldete Community anpassen + + - "Übersicht"-Dialog mit Filter auf angemeldete Community + - "Nutzersuche"-Dialog mit Filter auf angemeldete Community + - "Mehrfachschöpfung"-Dialog mit Filter auf angemeldete Comunity + - Subject/Texte/Footer/... der Email-Benachrichtigungen auf angemeldete Community anpassen ## Priorisierung -1. capturing alias -2. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption!!**! -3. Zeitzone -4. User-Beziehungen und Favoritenverwaltung +1. Contribution-Link editieren (vlt schon im vorherigen Bugfix-Release Ende Okt. 2022 fertig) +2. Passwort-Verschlüsselung: Refactoring **Konzeption fertig!!**! +3. Manuelle User-Registrierung für Admin (10.12.2022) **Konzeption ongoing!!**! +4. Passwort-Verschlüsselung: implizite Login-Neuverschlüsselung **Konzeption fertig!!**! 5. Layout -6. Passwort-Verschlüsselung -7. Subgruppierung / Subcommunities (einfacher Ansatz) -8. Contribution-Categories -9. backend access layer -10. Statistics / Analysen -11. technische Ablösung der Email und Ersatz durch GradidoID -12. Dezentralisierung / Federation - -## Zeitleiste - - - - -![img](./graphics/RoadMap2022-2023.png) +6. Zeitzone +7. Dezentralisierung / Federation +8. capturing alias **Konzeption fertig!!**! +9. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich +10. Subgruppierung / User-Tagging (einfacher Ansatz) +11. backend access layer +12. technische Ablösung der Email und Ersatz durch GradidoID +13. User-Beziehungen und Favoritenverwaltung +14. Lastschriften-Link +15. Contribution-Categories +16. Statistics / Analysen From 77227355d8760d8e481b04f66706c53c933d2256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 25 Oct 2022 22:27:30 +0200 Subject: [PATCH 06/47] =?UTF-8?q?kleine=20=C3=84nderungen=20u=20Erg=C3=A4n?= =?UTF-8?q?zungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docu/RoadMap_2022-2023.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docu/RoadMap_2022-2023.md b/docu/RoadMap_2022-2023.md index be3e197bd..fa8573448 100644 --- a/docu/RoadMap_2022-2023.md +++ b/docu/RoadMap_2022-2023.md @@ -101,10 +101,11 @@ - 01.11.2022 07:00 +09:00 => 01.11.2022 UTC: 31.10.2022 22:00 UTC => 31.10.2022 !!!! darf nicht weil gleicher Tag !!! 12. Layout 13. Lastschriften-Link -14. Registrierung mit Redeem-Link: bei inaktivem Konto keine Buchung möglich +14. Registrierung mit Redeem-Link: - 1. speichern des Links zusammen mit OptIn-Code - 2. + * bei inaktivem Konto, sprich bisher noch keine Email-Bestätigung, keine Buchung möglich + * somit speichern des Links zusammen mit OptIn-Code + * damit kann in einem Resend der ConfirmationEmail der Link auch korrekt wieder mitgeliefert werden 15. Manuelle User-Registrierung für Admin - soll am 10.12.2022 für den Tag bei den Galliern produktiv sein From 73ced2291e0cff0e0c5263520854ebf7283d244a Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 8 Nov 2022 13:41:33 +0100 Subject: [PATCH 07/47] database migration added --- backend/src/config/index.ts | 2 +- .../0053-change_password_encryption/User.ts | 127 ++++++++++++++++++ .../UserContact.ts | 66 +++++++++ database/entity/User.ts | 2 +- database/entity/UserContact.ts | 2 +- .../0053-change_password_encryption | 38 ++++++ 6 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 database/entity/0053-change_password_encryption/User.ts create mode 100644 database/entity/0053-change_password_encryption/UserContact.ts create mode 100644 database/migrations/0053-change_password_encryption diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7139033b..26227b90d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0052-add_updated_at_to_contributions', + DB_VERSION: '0053-change_password_encryption', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts new file mode 100644 index 000000000..bf2d02268 --- /dev/null +++ b/database/entity/0053-change_password_encryption/User.ts @@ -0,0 +1,127 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, + OneToOne, +} from 'typeorm' +import { Contribution } from '../Contribution' +import { ContributionMessage } from '../ContributionMessage' +import { UserContact } from '../UserContact' + +@Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class User extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'gradido_id', + length: 36, + nullable: false, + collation: 'utf8mb4_unicode_ci', + }) + gradidoID: string + + @Column({ + name: 'alias', + length: 20, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + alias: string + + /* + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string + */ + + @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) + @JoinColumn({ name: 'email_id' }) + emailContact: UserContact + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId: number | null + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ + name: 'password_encryption_type', + type: 'int', + unsigned: true, + nullable: false, + default: 1, + }) + passwordEncryptionType: number + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] + + @OneToMany(() => ContributionMessage, (message) => message.user) + @JoinColumn({ name: 'user_id' }) + messages?: ContributionMessage[] + + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) + @JoinColumn({ name: 'user_id' }) + userContacts?: UserContact[] +} diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts new file mode 100644 index 000000000..05bfdfffe --- /dev/null +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -0,0 +1,66 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToOne, + JoinColumn, + ManyToOne, +} from 'typeorm' +import { User } from './User' + +@Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) +export class UserContact extends BaseEntity { + @PrimaryGeneratedColumn('increment', { unsigned: true }) + id: number + + @Column({ + name: 'type', + length: 100, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + type: string + + @OneToOne(() => User, (user) => user.emailContact) + user: User + + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false }) + userId: number + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ name: 'email_verification_code', type: 'bigint', unsigned: true, unique: true }) + emailVerificationCode: BigInt + + @Column({ name: 'email_opt_in_type_id' }) + emailOptInTypeId: number + + @Column({ name: 'email_resend_count' }) + emailResendCount: number + + // @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + // emailHash: Buffer + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' }) + phone: string + + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' }) + updatedAt: Date | null + + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) + deletedAt: Date | null + + @ManyToOne(() => User, (user) => user.userContacts) + @JoinColumn({ name: 'user_id' }) + contactUser: User +} diff --git a/database/entity/User.ts b/database/entity/User.ts index d073f428a..b3c00a9b4 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0049-add_user_contacts_table/User' +export { User } from './0053-change_password_encryption/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index a368bb7ca..dd74e65c4 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0049-add_user_contacts_table/UserContact' +export { UserContact } from './0053-change_password_encryption/UserContact' diff --git a/database/migrations/0053-change_password_encryption b/database/migrations/0053-change_password_encryption new file mode 100644 index 000000000..1b87e2511 --- /dev/null +++ b/database/migrations/0053-change_password_encryption @@ -0,0 +1,38 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds and renames columns to and in the table `users` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users RENAME COLUMN created TO created_at;') + await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;') + // alter table emp rename column emp_name to name + await queryFn( + 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 1 AFTER password;', + ) + + // TODO these steps comes after verification and test + /* + await queryFn('ALTER TABLE users DROP COLUMN public_key;') + await queryFn('ALTER TABLE users DROP COLUMN privkey;') + await queryFn('ALTER TABLE users DROP COLUMN email_hash;') + await queryFn('ALTER TABLE users DROP COLUMN passphrase;') + */ +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;') + await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;') + await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;') + + // TODO these steps comes after verification and test + /* + await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') + await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') + */ +} \ No newline at end of file From ac6b8e666ff893d19786b51f20bff36b6c771038 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 9 Nov 2022 10:46:01 +0100 Subject: [PATCH 08/47] encryption interface --- .../graphql/enum/PasswordEncryptionType.ts | 12 +++++ backend/src/password/EmailEncryptr.ts | 19 +++++++ backend/src/password/EncryptorUtils.ts | 52 +++++++++++++++++++ backend/src/password/GradidoIDEncryptr.ts | 19 +++++++ backend/src/password/PasswordEncryptr.ts | 6 +++ ...ion => 0053-change_password_encryption.ts} | 4 +- 6 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 backend/src/graphql/enum/PasswordEncryptionType.ts create mode 100644 backend/src/password/EmailEncryptr.ts create mode 100644 backend/src/password/EncryptorUtils.ts create mode 100644 backend/src/password/GradidoIDEncryptr.ts create mode 100644 backend/src/password/PasswordEncryptr.ts rename database/migrations/{0053-change_password_encryption => 0053-change_password_encryption.ts} (97%) diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts new file mode 100644 index 000000000..4f23aa693 --- /dev/null +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -0,0 +1,12 @@ +import { registerEnumType } from 'type-graphql' + +export enum PasswordEncryptionType { + EMAIL = 0, + ONE_TIME = 1, + GRADIDO_ID = 2, +} + +registerEnumType(PasswordEncryptionType, { + name: 'PasswordEncryptionType', // this one is mandatory + description: 'Type of the password encryption', // this one is optional +}) diff --git a/backend/src/password/EmailEncryptr.ts b/backend/src/password/EmailEncryptr.ts new file mode 100644 index 000000000..59098e207 --- /dev/null +++ b/backend/src/password/EmailEncryptr.ts @@ -0,0 +1,19 @@ +import { User } from '@entity/User' +import { PasswordEncryptr } from './PasswordEncryptr' +import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' + +export class EmailEncryptr implements PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.emailContact.email, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + + return passwordHash + } + + async verifyPassword(dbUser: User, password: string): Promise { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } +} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts new file mode 100644 index 000000000..6609ff075 --- /dev/null +++ b/backend/src/password/EncryptorUtils.ts @@ -0,0 +1,52 @@ +import CONFIG from '@/config' +import { backendLogger as logger } from '@/server/logger' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const sodium = require('sodium-native') + +// We will reuse this for changePassword +export const isValidPassword = (password: string): boolean => { + return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) +} + +export const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { + logger.trace('SecretKeyCryptographyCreateKey...') + const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') + const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') + if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { + logger.error( + `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + ) + throw new Error( + `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, + ) + } + + const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) + sodium.crypto_hash_sha512_init(state) + sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) + sodium.crypto_hash_sha512_update(state, configLoginAppSecret) + const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) + sodium.crypto_hash_sha512_final(state, hash) + + const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) + const opsLimit = 10 + const memLimit = 33554432 + const algo = 2 + sodium.crypto_pwhash( + encryptionKey, + Buffer.from(password), + hash.slice(0, sodium.crypto_pwhash_SALTBYTES), + opsLimit, + memLimit, + algo, + ) + + const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) + sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) + + logger.debug( + `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, + ) + return [encryptionKeyHash, encryptionKey] +} diff --git a/backend/src/password/GradidoIDEncryptr.ts b/backend/src/password/GradidoIDEncryptr.ts new file mode 100644 index 000000000..630bee056 --- /dev/null +++ b/backend/src/password/GradidoIDEncryptr.ts @@ -0,0 +1,19 @@ +import { User } from '@entity/User' +import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' +import { PasswordEncryptr } from './PasswordEncryptr' + +export class GradidoIDEncryptr implements PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.gradidoID, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + + return passwordHash + } + + async verifyPassword(dbUser: User, password: string): Promise { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } +} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts new file mode 100644 index 000000000..24d949be9 --- /dev/null +++ b/backend/src/password/PasswordEncryptr.ts @@ -0,0 +1,6 @@ +import { User } from '@entity/User' + +export interface PasswordEncryptr { + encryptPassword(dbUser: User, password: string): Promise + verifyPassword(dbUser: User, password: string): Promise +} diff --git a/database/migrations/0053-change_password_encryption b/database/migrations/0053-change_password_encryption.ts similarity index 97% rename from database/migrations/0053-change_password_encryption rename to database/migrations/0053-change_password_encryption.ts index 1b87e2511..5d880689f 100644 --- a/database/migrations/0053-change_password_encryption +++ b/database/migrations/0053-change_password_encryption.ts @@ -1,4 +1,4 @@ -/* MIGRATION TO ADD GRADIDO_ID +/* MIGRATION TO ADD ENCRYPTION TO PASSWORDS * * This migration adds and renames columns to and in the table `users` */ @@ -35,4 +35,4 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') */ -} \ No newline at end of file +} From 926c2be4e774deef21bd8c69bf7bb2d9866df4bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 11 Nov 2022 00:20:35 +0100 Subject: [PATCH 09/47] add federation config properties --- backend/.env.dist | 11 +++++++++-- backend/.env.template | 7 ++++++- backend/src/config/index.ts | 8 +++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 3b6fe2ce4..29db81322 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v11.2022-10-27 +CONFIG_VERSION=v12.2022-11-10 # Server PORT=4000 @@ -63,5 +63,12 @@ EVENT_PROTOCOL_DISABLED=false # DHT # if you set this value, the DHT hyperswarm will start to announce and listen -# on an hash created from this tpoic +# on an hash created from this topic # DHT_TOPIC=GRADIDO_HUB +# Define the following federation properties to individualize this community instance +# for federation per hyperswarm +# FEDERATE_COMMUNITY_NAME=Gradido Community Development +# FEDERATE_COMMUNITY_URL==http://localhost/ +# FEDERATE_COMMUNITY_PORT=4000 +# FEDERATE_COMMUNITY_UUID=14466b15-4631-4b1b-9110-dd6694356d9c +# FEDERATE_KEY_SECRET=a51ef8ac7ef1abf162fb7a65261acd7a diff --git a/backend/.env.template b/backend/.env.template index d009d08ff..69e486515 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -56,5 +56,10 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET # EventProtocol EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED -# DHT +# DHT/Federation DHT_TOPIC=$DHT_TOPIC +FEDERATE_COMMUNITY_NAME=$FEDERATE_COMMUNITY_NAME +FEDERATE_COMMUNITY_URL=$FEDERATE_COMMUNITY_URL +FEDERATE_COMMUNITY_PORT=$FEDERATE_COMMUNITY_PORT +FEDERATE_COMMUNITY_UUID=$FEDERATE_COMMUNITY_UUID +FEDERATE_KEY_SECRET=$FEDERATE_KEY_SECRET diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index e7139033b..78a3319ed 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -17,7 +17,7 @@ const constants = { LOG_LEVEL: process.env.LOG_LEVEL || 'info', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v11.2022-10-27', + EXPECTED: 'v12.2022-11-10', CURRENT: '', }, } @@ -118,6 +118,12 @@ if ( const federation = { DHT_TOPIC: process.env.DHT_TOPIC || null, + FEDERATE_COMMUNITY_NAME: process.env.FEDERATE_COMMUNITY_NAME || community.COMMUNITY_NAME, + FEDERATE_COMMUNITY_URL: process.env.FEDERATE_COMMUNITY_URL || community.COMMUNITY_URL, + FEDERATE_COMMUNITY_PORT: process.env.FEDERATE_COMMUNITY_PORT || server.PORT, + FEDERATE_COMMUNITY_UUID: + process.env.FEDERATE_COMMUNITY_UUID || '14466b15-4631-4b1b-9110-dd6694356d9c', + FEDERATE_KEY_SECRET: process.env.FEDERATE_KEY_SECRET || loginServer.LOGIN_SERVER_KEY, } const CONFIG = { From 885099aa75ffd2eb3f57f292f0a85838b19f88c4 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 12 Nov 2022 19:57:53 +0100 Subject: [PATCH 10/47] moved all jest related packages in the `devDependencies` section, since thats where they belong --- backend/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/package.json b/backend/package.json index 1db683b2a..ac8022268 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,11 +19,9 @@ }, "dependencies": { "@hyperswarm/dht": "^6.2.0", - "@types/jest": "^27.0.2", "@types/lodash.clonedeep": "^4.5.6", "@types/uuid": "^8.3.4", "apollo-server-express": "^2.25.2", - "apollo-server-testing": "^2.25.2", "axios": "^0.21.1", "class-validator": "^0.13.1", "cors": "^2.8.5", @@ -32,7 +30,6 @@ "dotenv": "^10.0.0", "express": "^4.17.1", "graphql": "^15.5.1", - "jest": "^27.2.4", "jsonwebtoken": "^8.5.1", "lodash.clonedeep": "^4.5.0", "log4js": "^6.4.6", @@ -41,18 +38,19 @@ "random-bigint": "^0.0.1", "reflect-metadata": "^0.1.13", "sodium-native": "^3.3.0", - "ts-jest": "^27.0.5", "type-graphql": "^1.1.1", "uuid": "^8.3.2" }, "devDependencies": { "@types/express": "^4.17.12", "@types/faker": "^5.5.9", + "@types/jest": "^27.0.2", "@types/jsonwebtoken": "^8.5.2", "@types/node": "^16.10.3", "@types/nodemailer": "^6.4.4", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", + "apollo-server-testing": "^2.25.2", "eslint": "^7.29.0", "eslint-config-prettier": "^8.3.0", "eslint-config-standard": "^16.0.3", @@ -60,10 +58,12 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", + "jest": "^27.2.4", "faker": "^5.5.3", "nodemon": "^2.0.7", "prettier": "^2.3.1", "ts-node": "^10.0.0", + "ts-jest": "^27.0.5", "tsconfig-paths": "^3.14.0", "typescript": "^4.3.4" } From 48f66dc7d2f601e49ce5f5773727ccf852057711 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 12 Nov 2022 20:02:29 +0100 Subject: [PATCH 11/47] use proper order (alphabetical) --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index ac8022268..5c0de34f3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -62,8 +62,8 @@ "faker": "^5.5.3", "nodemon": "^2.0.7", "prettier": "^2.3.1", - "ts-node": "^10.0.0", "ts-jest": "^27.0.5", + "ts-node": "^10.0.0", "tsconfig-paths": "^3.14.0", "typescript": "^4.3.4" } From 3dfffbdfea14502b0b5744364ddbd8db67c7807b Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 12 Nov 2022 20:03:45 +0100 Subject: [PATCH 12/47] more order fixes --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 5c0de34f3..b061ce0d3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -58,8 +58,8 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.4.0", "eslint-plugin-promise": "^5.1.0", - "jest": "^27.2.4", "faker": "^5.5.3", + "jest": "^27.2.4", "nodemon": "^2.0.7", "prettier": "^2.3.1", "ts-jest": "^27.0.5", From 78451f0d9f5aab5fad98e41faae9958ca7c4da0c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 12 Nov 2022 20:05:09 +0100 Subject: [PATCH 13/47] move type definitions into `devDependencies` --- backend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/package.json b/backend/package.json index b061ce0d3..652650d56 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,8 +19,6 @@ }, "dependencies": { "@hyperswarm/dht": "^6.2.0", - "@types/lodash.clonedeep": "^4.5.6", - "@types/uuid": "^8.3.4", "apollo-server-express": "^2.25.2", "axios": "^0.21.1", "class-validator": "^0.13.1", @@ -46,8 +44,10 @@ "@types/faker": "^5.5.9", "@types/jest": "^27.0.2", "@types/jsonwebtoken": "^8.5.2", + "@types/lodash.clonedeep": "^4.5.6", "@types/node": "^16.10.3", "@types/nodemailer": "^6.4.4", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^4.28.0", "@typescript-eslint/parser": "^4.28.0", "apollo-server-testing": "^2.25.2", From 107cc016ba0064462ccde429699a1fb711f61508 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 15 Nov 2022 20:30:09 +0100 Subject: [PATCH 14/47] set up completed and working --- .../graphql/enum/PasswordEncryptionType.ts | 4 +-- .../src/graphql/resolver/UserResolver.test.ts | 1 + backend/src/password/EmailEncryptr.ts | 19 ------------ backend/src/password/EncryptorUtils.ts | 14 +++++++++ backend/src/password/GradidoIDEncryptr.ts | 19 ------------ backend/src/password/PasswordEncryptr.ts | 30 +++++++++++++++++-- backend/src/util/communityUser.ts | 2 ++ .../0053-change_password_encryption/User.ts | 28 ++++++++--------- .../UserContact.ts | 6 ---- 9 files changed, 60 insertions(+), 63 deletions(-) delete mode 100644 backend/src/password/EmailEncryptr.ts delete mode 100644 backend/src/password/GradidoIDEncryptr.ts diff --git a/backend/src/graphql/enum/PasswordEncryptionType.ts b/backend/src/graphql/enum/PasswordEncryptionType.ts index 4f23aa693..b3a00d748 100644 --- a/backend/src/graphql/enum/PasswordEncryptionType.ts +++ b/backend/src/graphql/enum/PasswordEncryptionType.ts @@ -1,8 +1,8 @@ import { registerEnumType } from 'type-graphql' export enum PasswordEncryptionType { - EMAIL = 0, - ONE_TIME = 1, + NO_PASSWORD = 0, + EMAIL = 1, GRADIDO_ID = 2, } diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index cf4ad8d4b..791ed4c8e 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -146,6 +146,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, + passwordEncryptionType: 1, }, ]) const valUUID = validateUUID(user[0].gradidoID) diff --git a/backend/src/password/EmailEncryptr.ts b/backend/src/password/EmailEncryptr.ts deleted file mode 100644 index 59098e207..000000000 --- a/backend/src/password/EmailEncryptr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from '@entity/User' -import { PasswordEncryptr } from './PasswordEncryptr' -import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' - -export class EmailEncryptr implements PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.emailContact.email, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - - return passwordHash - } - - async verifyPassword(dbUser: User, password: string): Promise { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } -} diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 6609ff075..5f6f4b416 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -1,5 +1,7 @@ import CONFIG from '@/config' import { backendLogger as logger } from '@/server/logger' +import { User } from '@entity/User' +import { PasswordEncryptionType } from '@enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -50,3 +52,15 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): ) return [encryptionKeyHash, encryptionKey] } + +export const getBasicCryptographicKey = (dbUser: User): string | null => { + if (dbUser.passwordEncryptionType === PasswordEncryptionType.NO_PASSWORD) { + return null + } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.EMAIL) { + return dbUser.emailContact.email + } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.GRADIDO_ID) { + return dbUser.gradidoID + } + + throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) +} diff --git a/backend/src/password/GradidoIDEncryptr.ts b/backend/src/password/GradidoIDEncryptr.ts deleted file mode 100644 index 630bee056..000000000 --- a/backend/src/password/GradidoIDEncryptr.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { User } from '@entity/User' -import { SecretKeyCryptographyCreateKey } from './EncryptorUtils' -import { PasswordEncryptr } from './PasswordEncryptr' - -export class GradidoIDEncryptr implements PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const keyBuffer = SecretKeyCryptographyCreateKey(dbUser.gradidoID, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - - return passwordHash - } - - async verifyPassword(dbUser: User, password: string): Promise { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } -} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts index 24d949be9..24dc7f352 100644 --- a/backend/src/password/PasswordEncryptr.ts +++ b/backend/src/password/PasswordEncryptr.ts @@ -1,6 +1,30 @@ import { User } from '@entity/User' +import { logger } from '@test/testSetup' +import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export interface PasswordEncryptr { - encryptPassword(dbUser: User, password: string): Promise - verifyPassword(dbUser: User, password: string): Promise +export class PasswordEncryptr { + async encryptPassword(dbUser: User, password: string): Promise { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) logger.error('Password not set for user ' + dbUser.id) + else { + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash + } + + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } + + async verifyPassword(dbUser: User, password: string): Promise { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) logger.error('Password not set for user ' + dbUser.id) + else { + if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { + return false + } + return true + } + + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index e885b7043..87d0432dc 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -26,6 +26,8 @@ const communityDbUser: dbUser = { isAdmin: null, publisherId: 0, passphrase: '', + // default password encryption type + passwordEncryptionType: 2, hasId: function (): boolean { throw new Error('Function not implemented.') }, diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts index bf2d02268..fac4e1031 100644 --- a/database/entity/0053-change_password_encryption/User.ts +++ b/database/entity/0053-change_password_encryption/User.ts @@ -34,20 +34,20 @@ export class User extends BaseEntity { }) alias: string - /* - @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) - pubKey: Buffer - @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) - privKey: Buffer - @Column({ - type: 'text', - name: 'passphrase', - collation: 'utf8mb4_unicode_ci', - nullable: true, - default: null, - }) - passphrase: string - */ + @Column({ name: 'public_key', type: 'binary', length: 32, default: null, nullable: true }) + pubKey: Buffer + + @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) + privKey: Buffer + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string @OneToOne(() => UserContact, (emailContact: UserContact) => emailContact.user) @JoinColumn({ name: 'email_id' }) diff --git a/database/entity/0053-change_password_encryption/UserContact.ts b/database/entity/0053-change_password_encryption/UserContact.ts index 05bfdfffe..97b12d4cd 100644 --- a/database/entity/0053-change_password_encryption/UserContact.ts +++ b/database/entity/0053-change_password_encryption/UserContact.ts @@ -5,8 +5,6 @@ import { Column, DeleteDateColumn, OneToOne, - JoinColumn, - ManyToOne, } from 'typeorm' import { User } from './User' @@ -59,8 +57,4 @@ export class UserContact extends BaseEntity { @DeleteDateColumn({ name: 'deleted_at', nullable: true }) deletedAt: Date | null - - @ManyToOne(() => User, (user) => user.userContacts) - @JoinColumn({ name: 'user_id' }) - contactUser: User } From 47d8469eb3292edbeeebcc4bb85273058b48afaf Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 17 Nov 2022 13:38:16 +0100 Subject: [PATCH 15/47] new password implementation set up and test --- .../src/graphql/resolver/UserResolver.test.ts | 65 ++++++++++++++++- backend/src/graphql/resolver/UserResolver.ts | 71 +++++-------------- backend/src/password/PasswordEncryptr.ts | 46 ++++++------ backend/src/util/communityUser.ts | 2 +- 4 files changed, 103 insertions(+), 81 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 2d92e196c..d3bfb1c66 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,6 +36,10 @@ import { UserContact } from '@entity/UserContact' import { OptInType } from '../enum/OptInType' import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' +import { encryptPassword } from '@/password/PasswordEncryptr' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' +import { find } from 'lodash' +import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -491,7 +495,8 @@ describe('UserResolver', () => { }) it('updates the password', () => { - expect(newUser.password).toEqual('3917921995996627700') + const encryptedPass = encryptPassword(newUser, 'Aa12345_') + expect(newUser.password.toString()).toEqual(encryptedPass.toString()) }) /* @@ -1159,6 +1164,64 @@ describe('UserResolver', () => { }) }) }) + + describe('password encryption type', () => { + describe('user just registered', () => { + let bibi: User + + it('password type should be gradido id', async () => { + const users = await User.find() + bibi = users[1] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + + describe('user has encryption type email', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + publisherId: 1234, + } + + let bibi: User + beforeAll(async () => { + const users = await User.find() + bibi = users[1] + + bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL + bibi.password = SecretKeyCryptographyCreateKey( + 'bibi@bloxberg.de', + 'Aa12345_', + )[0].readBigUInt64LE() + + await bibi.save() + }) + + it('changes to gradidoID on login', async () => { + await mutate({ mutation: login, variables: variables }) + + const users = await User.find() + bibi = users[0] + + expect(bibi).toEqual( + expect.objectContaining({ + password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] + .readBigUInt64LE() + .toString(), + passwordEncryptionType: PasswordEncryptionType.GRADIDO_ID, + }), + ) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2287ede98..78f8a96f8 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -39,17 +39,15 @@ import { SearchAdminUsersResult } from '@model/AdminUser' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { v4 as uuidv4 } from 'uuid' +import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptr' +import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') // eslint-disable-next-line @typescript-eslint/no-var-requires const random = require('random-bigint') -// We will reuse this for changePassword -const isPassword = (password: string): boolean => { - return !!password.match(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^a-zA-Z0-9 \\t\\n\\r]).{8,}$/) -} - const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] const DEFAULT_LANGUAGE = 'de' const isLanguage = (language: string): boolean => { @@ -106,48 +104,6 @@ const KeyPairEd25519Create = (passphrase: string[]): Buffer[] => { return [pubKey, privKey] } -const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[] => { - logger.trace('SecretKeyCryptographyCreateKey...') - const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex') - const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex') - if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) { - logger.error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - throw new Error( - `ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`, - ) - } - - const state = Buffer.alloc(sodium.crypto_hash_sha512_STATEBYTES) - sodium.crypto_hash_sha512_init(state) - sodium.crypto_hash_sha512_update(state, Buffer.from(salt)) - sodium.crypto_hash_sha512_update(state, configLoginAppSecret) - const hash = Buffer.alloc(sodium.crypto_hash_sha512_BYTES) - sodium.crypto_hash_sha512_final(state, hash) - - const encryptionKey = Buffer.alloc(sodium.crypto_box_SEEDBYTES) - const opsLimit = 10 - const memLimit = 33554432 - const algo = 2 - sodium.crypto_pwhash( - encryptionKey, - Buffer.from(password), - hash.slice(0, sodium.crypto_pwhash_SALTBYTES), - opsLimit, - memLimit, - algo, - ) - - const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) - sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - - logger.debug( - `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, - ) - return [encryptionKeyHash, encryptionKey] -} - /* const getEmailHash = (email: string): Buffer => { logger.trace('getEmailHash...') @@ -343,12 +299,16 @@ export class UserResolver { // TODO we want to catch this on the frontend and ask the user to check his emails or resend code throw new Error('User has no private or publicKey') } - const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash - const loginUserPassword = BigInt(dbUser.password.toString()) - if (loginUserPassword !== passwordHash[0].readBigUInt64LE()) { + + if (!verifyPassword(dbUser, password)) { logger.error('The User has no valid credentials.') throw new Error('No user with this credentials') } + + if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { + dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID + dbUser.password = encryptPassword(dbUser, password) + } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message logger.addContext('user', dbUser.id) logger.debug('validation of login credentials successful...') @@ -623,7 +583,7 @@ export class UserResolver { ): Promise { logger.info(`setPassword(${code}, ***)...`) // Validate Password - if (!isPassword(password)) { + if (!isValidPassword(password)) { logger.error('Password entered is lexically invalid') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -681,10 +641,11 @@ export class UserResolver { userContact.emailChecked = true // Update Password + user.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID const passwordHash = SecretKeyCryptographyCreateKey(userContact.email, password) // return short and long hash const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) - user.password = passwordHash[0].readBigUInt64LE() // using the shorthash + user.password = encryptPassword(user, password) user.pubKey = keyPair[0] user.privKey = encryptedPrivkey logger.debug('User credentials updated ...') @@ -789,7 +750,7 @@ export class UserResolver { if (password && passwordNew) { // Validate Password - if (!isPassword(passwordNew)) { + if (!isValidPassword(passwordNew)) { logger.error('newPassword does not fullfil the rules') throw new Error( 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', @@ -801,7 +762,7 @@ export class UserResolver { userEntity.emailContact.email, password, ) - if (BigInt(userEntity.password.toString()) !== oldPasswordHash[0].readBigUInt64LE()) { + if (!verifyPassword(userEntity, password)) { logger.error(`Old password is invalid`) throw new Error(`Old password is invalid`) } @@ -817,7 +778,7 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key - userEntity.password = newPasswordHash[0].readBigUInt64LE() + userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.privKey = encryptedPrivkey } diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts index 24dc7f352..b8ef2de31 100644 --- a/backend/src/password/PasswordEncryptr.ts +++ b/backend/src/password/PasswordEncryptr.ts @@ -1,30 +1,28 @@ import { User } from '@entity/User' -import { logger } from '@test/testSetup' +// import { logger } from '@test/testSetup' getting error "jest is not defined" import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' -export class PasswordEncryptr { - async encryptPassword(dbUser: User, password: string): Promise { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) logger.error('Password not set for user ' + dbUser.id) - else { - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - return passwordHash - } - - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } - - async verifyPassword(dbUser: User, password: string): Promise { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) logger.error('Password not set for user ' + dbUser.id) - else { - if (BigInt(password) !== (await this.encryptPassword(dbUser, dbUser.password.toString()))) { - return false - } - return true - } - +export const encryptPassword = (dbUser: User, password: string): bigint => { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) { + // logger.error('Password not set for user ' + dbUser.id) throw new Error('Password not set for user ' + dbUser.id) // user has no password + } else { + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash + } +} + +export const verifyPassword = (dbUser: User, password: string): boolean => { + const basicKey = getBasicCryptographicKey(dbUser) + if (!basicKey) { + // logger.error('Password not set for user ' + dbUser.id) + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } else { + if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { + return false + } + return true } } diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 87d0432dc..9913418fb 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -27,7 +27,7 @@ const communityDbUser: dbUser = { publisherId: 0, passphrase: '', // default password encryption type - passwordEncryptionType: 2, + passwordEncryptionType: 0, hasId: function (): boolean { throw new Error('Function not implemented.') }, From 39d006351d8e6c04bdaa6b6025c9e7898c724326 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 17 Nov 2022 14:10:17 +0100 Subject: [PATCH 16/47] unused import added wrongly --- backend/src/graphql/resolver/UserResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d3bfb1c66..03aabfbb4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -38,7 +38,6 @@ import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptr' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' -import { find } from 'lodash' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' // import { klicktippSignIn } from '@/apis/KlicktippController' 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 17/47] 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 fc23fc9e87768e0fc5f5f57235261c18b4518f54 Mon Sep 17 00:00:00 2001 From: joseji Date: Sun, 20 Nov 2022 18:14:43 +0100 Subject: [PATCH 18/47] fixes done --- .../src/graphql/resolver/UserResolver.test.ts | 6 ++-- backend/src/graphql/resolver/UserResolver.ts | 4 ++- backend/src/password/EncryptorUtils.ts | 27 ++++++++++-------- backend/src/password/PasswordEncryptor.ts | 17 +++++++++++ backend/src/password/PasswordEncryptr.ts | 28 ------------------- .../0053-change_password_encryption/User.ts | 2 +- .../0053-change_password_encryption.ts | 22 ++------------- 7 files changed, 42 insertions(+), 64 deletions(-) create mode 100644 backend/src/password/PasswordEncryptor.ts delete mode 100644 backend/src/password/PasswordEncryptr.ts diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 03aabfbb4..a3e2dbe21 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -36,7 +36,7 @@ import { UserContact } from '@entity/UserContact' import { OptInType } from '../enum/OptInType' import { UserContactType } from '../enum/UserContactType' import { bobBaumeister } from '@/seeds/users/bob-baumeister' -import { encryptPassword } from '@/password/PasswordEncryptr' +import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' @@ -149,7 +149,7 @@ describe('UserResolver', () => { publisherId: 1234, referrerId: null, contributionLinkId: null, - passwordEncryptionType: 1, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, }, ]) const valUUID = validateUUID(user[0].gradidoID) @@ -1168,7 +1168,7 @@ describe('UserResolver', () => { describe('user just registered', () => { let bibi: User - it('password type should be gradido id', async () => { + it('has password type gradido id', async () => { const users = await User.find() bibi = users[1] diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ef5edd747..a9006bbb6 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -40,7 +40,7 @@ import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { v4 as uuidv4 } from 'uuid' import { isValidPassword, SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' -import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptr' +import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -432,6 +432,7 @@ export class UserResolver { dbUser.lastName = lastName dbUser.language = language dbUser.publisherId = publisherId + dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD dbUser.passphrase = passphrase.join(' ') logger.debug('new dbUser=' + dbUser) if (redeemCode) { @@ -780,6 +781,7 @@ export class UserResolver { logger.debug('PrivateKey encrypted...') // Save new password hash and newly encrypted private key + userEntity.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID userEntity.password = encryptPassword(userEntity, passwordNew) userEntity.privKey = encryptedPrivkey } diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 5f6f4b416..2ca47109d 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -47,20 +47,23 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): const encryptionKeyHash = Buffer.alloc(sodium.crypto_shorthash_BYTES) sodium.crypto_shorthash(encryptionKeyHash, encryptionKey, configLoginServerKey) - logger.debug( - `SecretKeyCryptographyCreateKey...successful: encryptionKeyHash= ${encryptionKeyHash}, encryptionKey= ${encryptionKey}`, - ) return [encryptionKeyHash, encryptionKey] } -export const getBasicCryptographicKey = (dbUser: User): string | null => { - if (dbUser.passwordEncryptionType === PasswordEncryptionType.NO_PASSWORD) { - return null - } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.EMAIL) { - return dbUser.emailContact.email - } else if (dbUser.passwordEncryptionType === PasswordEncryptionType.GRADIDO_ID) { - return dbUser.gradidoID +export const getUserCryptographicSalt = (dbUser: User): string => { + switch (dbUser.passwordEncryptionType) { + case PasswordEncryptionType.NO_PASSWORD: { + throw new Error('Password not set for user ' + dbUser.id) // user has no password + } + case PasswordEncryptionType.EMAIL: { + return dbUser.emailContact.email + break + } + case PasswordEncryptionType.GRADIDO_ID: { + return dbUser.gradidoID + break + } + default: + throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) } - - throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) } diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts new file mode 100644 index 000000000..2c6ebfb0f --- /dev/null +++ b/backend/src/password/PasswordEncryptor.ts @@ -0,0 +1,17 @@ +import { User } from '@entity/User' +// import { logger } from '@test/testSetup' getting error "jest is not defined" +import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' + +export const encryptPassword = (dbUser: User, password: string): bigint => { + const basicKey = getUserCryptographicSalt(dbUser) + const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const passwordHash = keyBuffer[0].readBigUInt64LE() + return passwordHash +} + +export const verifyPassword = (dbUser: User, password: string): boolean => { + if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { + return false + } + return true +} diff --git a/backend/src/password/PasswordEncryptr.ts b/backend/src/password/PasswordEncryptr.ts deleted file mode 100644 index b8ef2de31..000000000 --- a/backend/src/password/PasswordEncryptr.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { User } from '@entity/User' -// import { logger } from '@test/testSetup' getting error "jest is not defined" -import { getBasicCryptographicKey, SecretKeyCryptographyCreateKey } from './EncryptorUtils' - -export const encryptPassword = (dbUser: User, password: string): bigint => { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) { - // logger.error('Password not set for user ' + dbUser.id) - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } else { - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash - const passwordHash = keyBuffer[0].readBigUInt64LE() - return passwordHash - } -} - -export const verifyPassword = (dbUser: User, password: string): boolean => { - const basicKey = getBasicCryptographicKey(dbUser) - if (!basicKey) { - // logger.error('Password not set for user ' + dbUser.id) - throw new Error('Password not set for user ' + dbUser.id) // user has no password - } else { - if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { - return false - } - return true - } -} diff --git a/database/entity/0053-change_password_encryption/User.ts b/database/entity/0053-change_password_encryption/User.ts index fac4e1031..2a3332925 100644 --- a/database/entity/0053-change_password_encryption/User.ts +++ b/database/entity/0053-change_password_encryption/User.ts @@ -88,7 +88,7 @@ export class User extends BaseEntity { type: 'int', unsigned: true, nullable: false, - default: 1, + default: 0, }) passwordEncryptionType: number diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts index 5d880689f..0c8632186 100644 --- a/database/migrations/0053-change_password_encryption.ts +++ b/database/migrations/0053-change_password_encryption.ts @@ -1,6 +1,6 @@ -/* MIGRATION TO ADD ENCRYPTION TO PASSWORDS +/* MIGRATION TO ADD ENCRYPTION TYPE TO PASSWORDS * - * This migration adds and renames columns to and in the table `users` + * This migration adds and renames columns in the table `users` */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ @@ -11,28 +11,12 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn('ALTER TABLE users RENAME COLUMN deletedAt TO deleted_at;') // alter table emp rename column emp_name to name await queryFn( - 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 1 AFTER password;', + 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;', ) - - // TODO these steps comes after verification and test - /* - await queryFn('ALTER TABLE users DROP COLUMN public_key;') - await queryFn('ALTER TABLE users DROP COLUMN privkey;') - await queryFn('ALTER TABLE users DROP COLUMN email_hash;') - await queryFn('ALTER TABLE users DROP COLUMN passphrase;') - */ } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { await queryFn('ALTER TABLE users RENAME COLUMN created_at TO created;') await queryFn('ALTER TABLE users RENAME COLUMN deleted_at TO deletedAt;') await queryFn('ALTER TABLE users DROP COLUMN password_encryption_type;') - - // TODO these steps comes after verification and test - /* - await queryFn('ALTER TABLE users ADD COLUMN public_key binary(32) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN privkey binary(80) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN email_hash binary(32) DEFAULT NULL;') - await queryFn('ALTER TABLE users ADD COLUMN passphrase text DEFAULT NULL;') - */ } From 3aeb9dd0f17e8abdaaec37258bdf6b6dd2b22950 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 22 Nov 2022 11:12:07 +0100 Subject: [PATCH 19/47] fixes added --- .../src/graphql/resolver/UserResolver.test.ts | 24 +++++++++++++++++++ backend/src/password/EncryptorUtils.ts | 2 ++ backend/src/password/PasswordEncryptor.ts | 4 ++-- .../0053-change_password_encryption.ts | 2 ++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 4e05aadd6..377dfa131 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -39,6 +39,7 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' +import { tokenToString } from 'typescript' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -1220,6 +1221,29 @@ describe('UserResolver', () => { }), ) }) + + it('can login after password change', async () => { + resetToken() + expect(await mutate({ mutation: login, variables: variables })).toEqual( + expect.objectContaining({ + data: { + login: { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + hasElopage: false, + id: expect.any(Number), + isAdmin: null, + klickTipp: { + newsletterState: false, + }, + language: 'de', + lastName: 'Bloxberg', + publisherId: 1234, + }, + }, + }), + ) + }) }) }) }) diff --git a/backend/src/password/EncryptorUtils.ts b/backend/src/password/EncryptorUtils.ts index 2ca47109d..971b6a32e 100644 --- a/backend/src/password/EncryptorUtils.ts +++ b/backend/src/password/EncryptorUtils.ts @@ -53,6 +53,7 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string): export const getUserCryptographicSalt = (dbUser: User): string => { switch (dbUser.passwordEncryptionType) { case PasswordEncryptionType.NO_PASSWORD: { + logger.error('Password not set for user ' + dbUser.id) throw new Error('Password not set for user ' + dbUser.id) // user has no password } case PasswordEncryptionType.EMAIL: { @@ -64,6 +65,7 @@ export const getUserCryptographicSalt = (dbUser: User): string => { break } default: + logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`) } } diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 2c6ebfb0f..3dc0736df 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -3,8 +3,8 @@ import { User } from '@entity/User' import { getUserCryptographicSalt, SecretKeyCryptographyCreateKey } from './EncryptorUtils' export const encryptPassword = (dbUser: User, password: string): bigint => { - const basicKey = getUserCryptographicSalt(dbUser) - const keyBuffer = SecretKeyCryptographyCreateKey(basicKey, password) // return short and long hash + const salt = getUserCryptographicSalt(dbUser) + const keyBuffer = SecretKeyCryptographyCreateKey(salt, password) // return short and long hash const passwordHash = keyBuffer[0].readBigUInt64LE() return passwordHash } diff --git a/database/migrations/0053-change_password_encryption.ts b/database/migrations/0053-change_password_encryption.ts index 0c8632186..635109430 100644 --- a/database/migrations/0053-change_password_encryption.ts +++ b/database/migrations/0053-change_password_encryption.ts @@ -13,6 +13,8 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis await queryFn( 'ALTER TABLE users ADD COLUMN password_encryption_type int(10) NOT NULL DEFAULT 0 AFTER password;', ) + await queryFn(`UPDATE users SET password_encryption_type = 1 WHERE id IN + (SELECT user_id FROM user_contacts WHERE email_checked = 1)`) } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { From e55f516c512019b15081ad476278884b35631587 Mon Sep 17 00:00:00 2001 From: joseji Date: Tue, 22 Nov 2022 11:19:42 +0100 Subject: [PATCH 20/47] removed unused import --- backend/src/graphql/resolver/UserResolver.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 377dfa131..200ba8163 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -39,7 +39,6 @@ import { bobBaumeister } from '@/seeds/users/bob-baumeister' import { encryptPassword } from '@/password/PasswordEncryptor' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { SecretKeyCryptographyCreateKey } from '@/password/EncryptorUtils' -import { tokenToString } from 'typescript' // import { klicktippSignIn } from '@/apis/KlicktippController' From 60e3f628325b38cfd5798bf5ebbec00b6a95c99b Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 23 Nov 2022 11:32:27 +0100 Subject: [PATCH 21/47] enum use on community user initialization --- backend/src/util/communityUser.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 9913418fb..298348f0f 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import { PasswordEncryptionType } from '@/graphql/enum/PasswordEncryptionType' import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' import { UserContact } from '@entity/UserContact' @@ -27,7 +28,7 @@ const communityDbUser: dbUser = { publisherId: 0, passphrase: '', // default password encryption type - passwordEncryptionType: 0, + passwordEncryptionType: PasswordEncryptionType.NO_PASSWORD, hasId: function (): boolean { throw new Error('Function not implemented.') }, From a1e8d0b2061c631a4cf3d23e94ff96fa748d03c9 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 23 Nov 2022 17:57:22 +0100 Subject: [PATCH 22/47] 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 2a74b924721257b239a0148ec7bd8b28e424a8d5 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 24 Nov 2022 11:21:47 +0100 Subject: [PATCH 23/47] user saving on db --- .../src/graphql/resolver/UserResolver.test.ts | 16 +++++++++++----- backend/src/graphql/resolver/UserResolver.ts | 1 + backend/src/password/PasswordEncryptor.ts | 5 +---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 200ba8163..d8472fba9 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1193,9 +1193,11 @@ describe('UserResolver', () => { let bibi: User beforeAll(async () => { - const users = await User.find() - bibi = users[1] - + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user bibi.passwordEncryptionType = PasswordEncryptionType.EMAIL bibi.password = SecretKeyCryptographyCreateKey( 'bibi@bloxberg.de', @@ -1208,11 +1210,15 @@ describe('UserResolver', () => { it('changes to gradidoID on login', async () => { await mutate({ mutation: login, variables: variables }) - const users = await User.find() - bibi = users[0] + const usercontact = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + bibi = usercontact.user expect(bibi).toEqual( expect.objectContaining({ + firstName: 'Bibi', password: SecretKeyCryptographyCreateKey(bibi.gradidoID.toString(), 'Aa12345_')[0] .readBigUInt64LE() .toString(), diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index b376c632f..752c585fd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -311,6 +311,7 @@ export class UserResolver { if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) { dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID dbUser.password = encryptPassword(dbUser, password) + await dbUser.save() } // add pubKey in logger-context for layout-pattern X{user} to print it in each logging message logger.addContext('user', dbUser.id) diff --git a/backend/src/password/PasswordEncryptor.ts b/backend/src/password/PasswordEncryptor.ts index 3dc0736df..1735106c1 100644 --- a/backend/src/password/PasswordEncryptor.ts +++ b/backend/src/password/PasswordEncryptor.ts @@ -10,8 +10,5 @@ export const encryptPassword = (dbUser: User, password: string): bigint => { } export const verifyPassword = (dbUser: User, password: string): boolean => { - if (dbUser.password.toString() !== encryptPassword(dbUser, password).toString()) { - return false - } - return true + return dbUser.password.toString() === encryptPassword(dbUser, password).toString() } From b3e880d45f339293d1a8cb49d04ad85bd816ee82 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Nov 2022 13:03:50 +0100 Subject: [PATCH 24/47] undo refactoring --- .../resolver/TransactionLinkResolver.ts | 5 +- .../resolver/TransactionResolver.test.ts | 70 +++++++++---------- .../graphql/resolver/TransactionResolver.ts | 33 ++++----- backend/src/util/utilities.ts | 12 ---- backend/src/util/validate.ts | 35 +++------- 5 files changed, 59 insertions(+), 96 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index a5c4a5f01..1b3558bb2 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -74,7 +74,10 @@ export class TransactionLinkResolver { const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay) // validate amount - await calculateBalance(user.id, holdAvailableAmount, createdDate) + const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate) + if (!sendBalance) { + throw new Error("user hasn't enough GDD or amount is < 0") + } const transactionLink = dbTransactionLink.create() transactionLink.userId = user.id diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 9e74623c8..86cdf5314 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -16,7 +16,7 @@ import { stephenHawking } from '@/seeds/users/stephen-hawking' import { EventProtocol } from '@entity/EventProtocol' import { Transaction } from '@entity/Transaction' import { User } from '@entity/User' -import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { cleanDB, testEnvironment } from '@test/helpers' import { logger } from '@test/testSetup' import { GraphQLError } from 'graphql' import { findUserByEmail } from './UserResolver' @@ -253,50 +253,21 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError(`User has not received any GDD yet`)], + errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], }), ) }) it('logs the error thrown', () => { expect(logger.error).toBeCalledWith( - `No prior transaction found for user with id: ${user[1].id}`, + `user hasn't enough GDD or amount is < 0 : balance=null`, ) }) }) - - describe('sending negative amount', () => { - it('throws an error', async () => { - jest.clearAllMocks() - expect( - await mutate({ - mutation: sendCoins, - variables: { - email: 'peter@lustig.de', - amount: -50, - memo: 'testing negative', - }, - }), - ).toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Transaction amount must be greater than 0')], - }), - ) - }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith('Transaction amount must be greater than 0: -50') - }) - }) }) describe('user has some GDD', () => { beforeAll(async () => { - resetToken() - - // login as bob again - await query({ mutation: login, variables: bobData }) - // create contribution as user bob const contribution = await mutate({ mutation: createContribution, @@ -316,6 +287,35 @@ describe('send coins', () => { await query({ mutation: login, variables: bobData }) }) + afterAll(async () => { + await cleanDB() + }) + + describe('trying to send negative amount', () => { + it('throws an error', async () => { + expect( + await mutate({ + mutation: sendCoins, + variables: { + email: 'peter@lustig.de', + amount: -50, + memo: 'testing negative', + }, + }), + ).toEqual( + expect.objectContaining({ + errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)], + }), + ) + }) + + it('logs the error thrown', () => { + expect(logger.error).toBeCalledWith( + `user hasn't enough GDD or amount is < 0 : balance=null`, + ) + }) + }) + describe('good transaction', () => { it('sends the coins', async () => { expect( @@ -324,7 +324,7 @@ describe('send coins', () => { variables: { email: 'peter@lustig.de', amount: 50, - memo: 'unrepeatable memo', + memo: 'unrepeateable memo', }, }), ).toEqual( @@ -340,7 +340,7 @@ describe('send coins', () => { // Find the exact transaction (sent one is the one with user[1] as user) const transaction = await Transaction.find({ userId: user[1].id, - memo: 'unrepeatable memo', + memo: 'unrepeateable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( @@ -357,7 +357,7 @@ describe('send coins', () => { // Find the exact transaction (received one is the one with user[0] as user) const transaction = await Transaction.find({ userId: user[0].id, - memo: 'unrepeatable memo', + memo: 'unrepeateable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index f0fb2f452..594039cfd 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -39,7 +39,6 @@ import { findUserByEmail } from './UserResolver' import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { eventProtocol } from '@/event/EventProtocolEmitter' -import { Decay } from '../model/Decay' export const executeTransaction = async ( amount: Decimal, @@ -69,8 +68,17 @@ export const executeTransaction = async ( // validate amount const receivedCallDate = new Date() - - const sendBalance = await calculateBalance(sender.id, amount, receivedCallDate, transactionLink) + const sendBalance = await calculateBalance( + sender.id, + amount.mul(-1), + receivedCallDate, + transactionLink, + ) + logger.debug(`calculated Balance=${sendBalance}`) + if (!sendBalance) { + logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`) + throw new Error("user hasn't enough GDD or amount is < 0") + } const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() @@ -100,24 +108,7 @@ export const executeTransaction = async ( transactionReceive.userId = recipient.id transactionReceive.linkedUserId = sender.id transactionReceive.amount = amount - - // state received balance - let receiveBalance: { - balance: Decimal - decay: Decay - lastTransactionId: number - } | null - - // try received balance - try { - receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) - } catch (e) { - logger.info( - `User with no transactions sent: ${recipient.id}, has received a transaction of ${amount} GDD from user: ${sender.id}`, - ) - receiveBalance = null - } - + const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate) transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount transactionReceive.balanceDate = receivedCallDate transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index 65214ebb5..9abb31554 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -1,17 +1,5 @@ -import Decimal from 'decimal.js-light' - export const objectValuesToArray = (obj: { [x: string]: string }): Array => { return Object.keys(obj).map(function (key) { return obj[key] }) } - -// to improve code readability, as String is needed, it is handled inside this utility function -export const decimalAddition = (a: Decimal, b: Decimal): Decimal => { - return a.add(b.toString()) -} - -// to improve code readability, as String is needed, it is handled inside this utility function -export const decimalSubtraction = (a: Decimal, b: Decimal): Decimal => { - return a.minus(b.toString()) -} diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 9640cc614..8d1c90ca4 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -5,8 +5,6 @@ import { Decay } from '@model/Decay' import { getCustomRepository } from '@dbTools/typeorm' import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' -import { decimalSubtraction, decimalAddition } from './utilities' -import { backendLogger as logger } from '@/server/logger' function isStringBoolean(value: string): boolean { const lowerValue = value.toLowerCase() @@ -25,26 +23,14 @@ async function calculateBalance( amount: Decimal, time: Date, transactionLink?: dbTransactionLink | null, -): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number }> { - // negative or empty amount should not be allowed - if (amount.lessThanOrEqualTo(0)) { - logger.error(`Transaction amount must be greater than 0: ${amount}`) - throw new Error('Transaction amount must be greater than 0') - } - - // check if user has prior transactions +): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } }) - - if (!lastTransaction) { - logger.error(`No prior transaction found for user with id: ${userId}`) - throw new Error('User has not received any GDD yet') - } + if (!lastTransaction) return null const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) - // new balance is the old balance minus the amount used - const balance = decimalSubtraction(decay.balance, amount) - + // TODO why we have to use toString() here? + const balance = decay.balance.add(amount.toString()) const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time) @@ -52,16 +38,11 @@ async function calculateBalance( // else we cannot redeem links which are more or equal to half of what an account actually owns const releasedLinkAmount = transactionLink ? transactionLink.holdAvailableAmount : new Decimal(0) - const availableBalance = decimalSubtraction(balance, sumHoldAvailableAmount) - - if (decimalAddition(availableBalance, releasedLinkAmount).lessThan(0)) { - logger.error( - `Not enough funds for a transaction of ${amount} GDD, user with id: ${userId} has only ${balance} GDD available`, - ) - throw new Error('Not enough funds for transaction') + if ( + balance.minus(sumHoldAvailableAmount.toString()).plus(releasedLinkAmount.toString()).lessThan(0) + ) { + return null } - - logger.debug(`calculated Balance=${balance}`) return { balance, lastTransactionId: lastTransaction.id, decay } } From 77d93f18fa5c79982dd0b6b15de0fbcf986f80ca Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Nov 2022 13:13:01 +0100 Subject: [PATCH 25/47] remove test --- backend/src/graphql/resolver/TransactionResolver.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 86cdf5314..44ccc838a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -291,6 +291,7 @@ describe('send coins', () => { await cleanDB() }) + /* describe('trying to send negative amount', () => { it('throws an error', async () => { expect( @@ -315,6 +316,7 @@ describe('send coins', () => { ) }) }) + */ describe('good transaction', () => { it('sends the coins', async () => { From 63e22eeab80b41ca4204f6604dfa5b6d30898b29 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Nov 2022 16:30:55 +0100 Subject: [PATCH 26/47] fix(database): wrong balance and decay values --- .../0054-recalculate-balance-and-decay.ts | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 database/migrations/0054-recalculate-balance-and-decay.ts diff --git a/database/migrations/0054-recalculate-balance-and-decay.ts b/database/migrations/0054-recalculate-balance-and-decay.ts new file mode 100644 index 000000000..fe9896db1 --- /dev/null +++ b/database/migrations/0054-recalculate-balance-and-decay.ts @@ -0,0 +1,122 @@ +/* MIGRATION TO FIX WRONG BALANCE + * + * Due to a bug in the code + * the amount of a receive balance is substracted + * from the previous balance instead of added. + * + * Therefore all balance and decay fields must + * be recalculated + * + * WARNING: This Migration must be run in TZ=UTC + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import Decimal from 'decimal.js-light' + +// Set precision value +Decimal.set({ + precision: 25, + rounding: Decimal.ROUND_HALF_UP, +}) + +const DECAY_START_TIME = new Date('2021-05-13 17:46:31') // GMT+0 + +interface Decay { + balance: Decimal + decay: Decimal | null + start: Date | null + end: Date | null + duration: number | null +} + +export enum TransactionTypeId { + CREATION = 1, + SEND = 2, + RECEIVE = 3, +} + +function decayFormula(value: Decimal, seconds: number): Decimal { + return value.mul(new Decimal('0.99999997803504048973201202316767079413460520837376').pow(seconds)) +} + +function calculateDecay( + amount: Decimal, + from: Date, + to: Date, + startBlock: Date = DECAY_START_TIME, +): Decay { + const fromMs = from.getTime() + const toMs = to.getTime() + const startBlockMs = startBlock.getTime() + + if (toMs < fromMs) { + throw new Error('to < from, reverse decay calculation is invalid') + } + + // Initialize with no decay + const decay: Decay = { + balance: amount, + decay: null, + start: null, + end: null, + duration: null, + } + + // decay started after end date; no decay + if (startBlockMs > toMs) { + return decay + } + // decay started before start date; decay for full duration + if (startBlockMs < fromMs) { + decay.start = from + decay.duration = (toMs - fromMs) / 1000 + } + // decay started between start and end date; decay from decay start till end date + else { + decay.start = startBlock + decay.duration = (toMs - startBlockMs) / 1000 + } + + decay.end = to + decay.balance = decayFormula(amount, decay.duration) + decay.decay = decay.balance.minus(amount) + return decay +} + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // Find all users & loop over them + const users = await queryFn('SELECT user_id FROM transactions GROUP BY user_id;') + for (let u = 0; u < users.length; u++) { + // find all transactions for a user + const transactions = await queryFn( + `SELECT * FROM transactions WHERE user_id = ${users[u].user_id} ORDER BY balance_date ASC;`, + ) + let previous = null + let balance = new Decimal(0) + for (let t = 0; t < transactions.length; t++) { + const transaction = transactions[t] + + const decayStartDate = previous ? previous.balance_date : transaction.balance_date + const amount = new Decimal(transaction.amount) + const decay = calculateDecay(balance, decayStartDate, transaction.balance_date) + balance = decay.balance.add(amount) + + // Update + await queryFn(` + UPDATE transactions SET + balance = ${balance}, + decay = ${decay.decay ? decay.decay : 0} + WHERE id = ${transaction.id}; + `) + + // previous + previous = transaction + } + } +} + +/* 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 f2e88469325d1a188548d576367e923dbfa47d07 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Nov 2022 16:37:32 +0100 Subject: [PATCH 27/47] rename migration file, set new db version in backend --- backend/src/config/index.ts | 2 +- ...lance-and-decay.ts => 0054-recalculate_balance_and_decay.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename database/migrations/{0054-recalculate-balance-and-decay.ts => 0054-recalculate_balance_and_decay.ts} (100%) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 26227b90d..a66ed9765 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0053-change_password_encryption', + DB_VERSION: '0054-recalculate_balance_and_decay', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/database/migrations/0054-recalculate-balance-and-decay.ts b/database/migrations/0054-recalculate_balance_and_decay.ts similarity index 100% rename from database/migrations/0054-recalculate-balance-and-decay.ts rename to database/migrations/0054-recalculate_balance_and_decay.ts From b4fbb09f2657ce5e40d4456eaa35961e9492ab73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 00:57:48 +0100 Subject: [PATCH 28/47] define the federation config properties --- backend/.env.dist | 14 ++++---------- backend/.env.template | 8 ++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 29db81322..d53ebdac8 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -61,14 +61,8 @@ EVENT_PROTOCOL_DISABLED=false # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # LOG_LEVEL=info -# DHT -# if you set this value, the DHT hyperswarm will start to announce and listen +# Federation / DHT +# if you set the value of FEDERATE_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # on an hash created from this topic -# DHT_TOPIC=GRADIDO_HUB -# Define the following federation properties to individualize this community instance -# for federation per hyperswarm -# FEDERATE_COMMUNITY_NAME=Gradido Community Development -# FEDERATE_COMMUNITY_URL==http://localhost/ -# FEDERATE_COMMUNITY_PORT=4000 -# FEDERATE_COMMUNITY_UUID=14466b15-4631-4b1b-9110-dd6694356d9c -# FEDERATE_KEY_SECRET=a51ef8ac7ef1abf162fb7a65261acd7a +# FEDERATE_DHT_TOPIC=GRADIDO_HUB +# FEDERATE_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f diff --git a/backend/.env.template b/backend/.env.template index 69e486515..2aec0483c 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -57,9 +57,5 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED # DHT/Federation -DHT_TOPIC=$DHT_TOPIC -FEDERATE_COMMUNITY_NAME=$FEDERATE_COMMUNITY_NAME -FEDERATE_COMMUNITY_URL=$FEDERATE_COMMUNITY_URL -FEDERATE_COMMUNITY_PORT=$FEDERATE_COMMUNITY_PORT -FEDERATE_COMMUNITY_UUID=$FEDERATE_COMMUNITY_UUID -FEDERATE_KEY_SECRET=$FEDERATE_KEY_SECRET +FEDERATE_DHT_TOPIC=$FEDERATE_DHT_TOPIC +FEDERATE_DHT_SEED=$FEDERATE_DHT_SEED From cf1c68f290d5c8c67af3f718792edc328826eb85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 00:58:28 +0100 Subject: [PATCH 29/47] define the federation config properties --- backend/src/config/index.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 5ce0355e7..9206dcc04 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -117,13 +117,8 @@ if ( } const federation = { - DHT_TOPIC: process.env.DHT_TOPIC || null, - FEDERATE_COMMUNITY_NAME: process.env.FEDERATE_COMMUNITY_NAME || community.COMMUNITY_NAME, - FEDERATE_COMMUNITY_URL: process.env.FEDERATE_COMMUNITY_URL || community.COMMUNITY_URL, - FEDERATE_COMMUNITY_PORT: process.env.FEDERATE_COMMUNITY_PORT || server.PORT, - FEDERATE_COMMUNITY_UUID: - process.env.FEDERATE_COMMUNITY_UUID || '14466b15-4631-4b1b-9110-dd6694356d9c', - FEDERATE_KEY_SECRET: process.env.FEDERATE_KEY_SECRET || loginServer.LOGIN_SERVER_KEY, + FEDERATE_DHT_TOPIC: process.env.FEDERATE_DHT_TOPIC || null, + FEDERATE_DHT_SEED: process.env.FEDERATE_DHT_SEED || null, } const CONFIG = { From 28fa11bcd5b0cd833d9546c63cd801cd8ec7a970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 00:59:35 +0100 Subject: [PATCH 30/47] use the new config property to start DHT --- backend/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index dc1bbb115..69304f84e 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -19,8 +19,10 @@ async function main() { }) // start DHT hyperswarm when DHT_TOPIC is set in .env - if (CONFIG.DHT_TOPIC) { - await startDHT(CONFIG.DHT_TOPIC) // con, + if (CONFIG.FEDERATE_DHT_TOPIC) { + // eslint-disable-next-line no-console + console.log(`Federation/DHT active on ${CONFIG.FEDERATE_DHT_TOPIC}`) + await startDHT(CONFIG.FEDERATE_DHT_TOPIC) // con, } } From 58637366582c95ad1355f1655e89abfeb2a744e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 01:01:14 +0100 Subject: [PATCH 31/47] create DHT-KeyPair with optional configured Seed --- backend/src/federation/index.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/src/federation/index.ts b/backend/src/federation/index.ts index 2ca58b432..a7a06e42a 100644 --- a/backend/src/federation/index.ts +++ b/backend/src/federation/index.ts @@ -4,11 +4,24 @@ import DHT from '@hyperswarm/dht' // import { Connection } from '@dbTools/typeorm' import { backendLogger as logger } from '@/server/logger' +import CONFIG from '@/config' +// eslint-disable-next-line @typescript-eslint/no-var-requires +const sodium = require('sodium-native') function between(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min) } +const getSeed = (): Buffer | null => { + if (CONFIG.FEDERATE_DHT_SEED) { + const secret = CONFIG.FEDERATE_DHT_SEED + const seed = sodium.sodium_malloc(sodium.crypto_box_SEEDBYTES) + seed.write(secret, 'hex') + return seed + } + return null +} + const POLLTIME = 20000 const SUCCESSTIME = 120000 const ERRORTIME = 240000 @@ -28,7 +41,9 @@ export const startDHT = async ( try { const TOPIC = DHT.hash(Buffer.from(topic)) - const keyPair = DHT.keyPair() + const keyPair = DHT.keyPair(getSeed()) + logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) + logger.info(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) const node = new DHT({ keyPair }) From 68586eaae7e04d6f8d0304f049a424ff5521a9f0 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Fri, 25 Nov 2022 01:37:36 +0100 Subject: [PATCH 32/47] Update backend/.env.dist Co-authored-by: Ulf Gebhardt --- backend/.env.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.env.dist b/backend/.env.dist index d53ebdac8..94fb47fe8 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -61,7 +61,7 @@ EVENT_PROTOCOL_DISABLED=false # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # LOG_LEVEL=info -# Federation / DHT +# Federation # if you set the value of FEDERATE_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # on an hash created from this topic # FEDERATE_DHT_TOPIC=GRADIDO_HUB From ce1b488865b8067bc3da3dbd43bbaeee113c7838 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Fri, 25 Nov 2022 01:37:56 +0100 Subject: [PATCH 33/47] Update backend/.env.template Co-authored-by: Ulf Gebhardt --- backend/.env.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/.env.template b/backend/.env.template index 2aec0483c..1b566a05c 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -56,6 +56,6 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET # EventProtocol EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED -# DHT/Federation +# Federation FEDERATE_DHT_TOPIC=$FEDERATE_DHT_TOPIC FEDERATE_DHT_SEED=$FEDERATE_DHT_SEED From 192ab67817a12f0a765caa48c0e13272b7310b25 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Fri, 25 Nov 2022 01:43:59 +0100 Subject: [PATCH 34/47] Update backend/src/index.ts Co-authored-by: Ulf Gebhardt --- backend/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 69304f84e..b5ef4d611 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -21,7 +21,7 @@ async function main() { // start DHT hyperswarm when DHT_TOPIC is set in .env if (CONFIG.FEDERATE_DHT_TOPIC) { // eslint-disable-next-line no-console - console.log(`Federation/DHT active on ${CONFIG.FEDERATE_DHT_TOPIC}`) + console.log(`Federation active on ${CONFIG.FEDERATE_DHT_TOPIC}`) await startDHT(CONFIG.FEDERATE_DHT_TOPIC) // con, } } From d021cb29187506f6fba78ee0773eb8c5f2bc54e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 03:01:23 +0100 Subject: [PATCH 35/47] renamend porperties to FEDERATION_xxx --- backend/.env.dist | 8 ++++---- backend/.env.template | 6 +++--- backend/src/config/index.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index d53ebdac8..c0a2a6098 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -61,8 +61,8 @@ EVENT_PROTOCOL_DISABLED=false # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # LOG_LEVEL=info -# Federation / DHT -# if you set the value of FEDERATE_DHT_TOPIC, the DHT hyperswarm will start to announce and listen +# Federation +# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # on an hash created from this topic -# FEDERATE_DHT_TOPIC=GRADIDO_HUB -# FEDERATE_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f +# FEDERATION_DHT_TOPIC=GRADIDO_HUB +# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f diff --git a/backend/.env.template b/backend/.env.template index 2aec0483c..1bb2e4155 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -56,6 +56,6 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET # EventProtocol EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED -# DHT/Federation -FEDERATE_DHT_TOPIC=$FEDERATE_DHT_TOPIC -FEDERATE_DHT_SEED=$FEDERATE_DHT_SEED +# Federation +FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC +FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 9206dcc04..0512434f1 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -117,8 +117,8 @@ if ( } const federation = { - FEDERATE_DHT_TOPIC: process.env.FEDERATE_DHT_TOPIC || null, - FEDERATE_DHT_SEED: process.env.FEDERATE_DHT_SEED || null, + FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, + FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, } const CONFIG = { From 111602c707a0134962e4b7353b93843b3d8197c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 03:03:17 +0100 Subject: [PATCH 36/47] rework PR comments --- backend/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 69304f84e..35fb191cd 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -19,10 +19,10 @@ async function main() { }) // start DHT hyperswarm when DHT_TOPIC is set in .env - if (CONFIG.FEDERATE_DHT_TOPIC) { + if (CONFIG.FEDERATION_DHT_TOPIC) { // eslint-disable-next-line no-console - console.log(`Federation/DHT active on ${CONFIG.FEDERATE_DHT_TOPIC}`) - await startDHT(CONFIG.FEDERATE_DHT_TOPIC) // con, + console.log(`Federation active on ${CONFIG.FEDERATION_DHT_TOPIC}`) + await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con, } } From 7a2de6c437b16e35e95cc6c9a4ffa3dc95dd1dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 03:03:57 +0100 Subject: [PATCH 37/47] rework PR comments --- backend/src/federation/index.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/src/federation/index.ts b/backend/src/federation/index.ts index a7a06e42a..cbf78bc1c 100644 --- a/backend/src/federation/index.ts +++ b/backend/src/federation/index.ts @@ -5,23 +5,21 @@ import DHT from '@hyperswarm/dht' // import { Connection } from '@dbTools/typeorm' import { backendLogger as logger } from '@/server/logger' import CONFIG from '@/config' -// eslint-disable-next-line @typescript-eslint/no-var-requires -const sodium = require('sodium-native') function between(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min) } const getSeed = (): Buffer | null => { - if (CONFIG.FEDERATE_DHT_SEED) { - const secret = CONFIG.FEDERATE_DHT_SEED - const seed = sodium.sodium_malloc(sodium.crypto_box_SEEDBYTES) - seed.write(secret, 'hex') - return seed + if (CONFIG.FEDERATION_DHT_SEED) { + logger.debug(`with seed='${CONFIG.FEDERATION_DHT_SEED}'`) + return Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) } return null } +const KEY_SECRET_SEEDBYTES = 32 + const POLLTIME = 20000 const SUCCESSTIME = 120000 const ERRORTIME = 240000 @@ -40,10 +38,10 @@ export const startDHT = async ( ): Promise => { try { const TOPIC = DHT.hash(Buffer.from(topic)) - + logger.debug(`getSeed='${getSeed()}'`) const keyPair = DHT.keyPair(getSeed()) logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) - logger.info(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) + logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) const node = new DHT({ keyPair }) From efa46f50f3b27f415440fdec9ff8fdfb820e38db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 03:09:54 +0100 Subject: [PATCH 38/47] resolve conflicts --- backend/.env.dist | 4 ---- backend/.env.template | 5 ----- backend/src/federation/index.ts | 1 - 3 files changed, 10 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 7cc0e9737..c0a2a6098 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -62,11 +62,7 @@ EVENT_PROTOCOL_DISABLED=false # LOG_LEVEL=info # Federation -<<<<<<< HEAD # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen -======= -# if you set the value of FEDERATE_DHT_TOPIC, the DHT hyperswarm will start to announce and listen ->>>>>>> 192ab67817a12f0a765caa48c0e13272b7310b25 # on an hash created from this topic # FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f diff --git a/backend/.env.template b/backend/.env.template index f356082f8..1bb2e4155 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -57,10 +57,5 @@ WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED # Federation -<<<<<<< HEAD FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED -======= -FEDERATE_DHT_TOPIC=$FEDERATE_DHT_TOPIC -FEDERATE_DHT_SEED=$FEDERATE_DHT_SEED ->>>>>>> 192ab67817a12f0a765caa48c0e13272b7310b25 diff --git a/backend/src/federation/index.ts b/backend/src/federation/index.ts index cbf78bc1c..352724274 100644 --- a/backend/src/federation/index.ts +++ b/backend/src/federation/index.ts @@ -38,7 +38,6 @@ export const startDHT = async ( ): Promise => { try { const TOPIC = DHT.hash(Buffer.from(topic)) - logger.debug(`getSeed='${getSeed()}'`) const keyPair = DHT.keyPair(getSeed()) logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) From 9395b5a0ba866db84363a8afd375ee9fe827b2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 03:11:14 +0100 Subject: [PATCH 39/47] resolve conflicts --- backend/src/index.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 1be47eec2..35fb191cd 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -21,13 +21,8 @@ async function main() { // start DHT hyperswarm when DHT_TOPIC is set in .env if (CONFIG.FEDERATION_DHT_TOPIC) { // eslint-disable-next-line no-console -<<<<<<< HEAD console.log(`Federation active on ${CONFIG.FEDERATION_DHT_TOPIC}`) await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con, -======= - console.log(`Federation active on ${CONFIG.FEDERATE_DHT_TOPIC}`) - await startDHT(CONFIG.FEDERATE_DHT_TOPIC) // con, ->>>>>>> 192ab67817a12f0a765caa48c0e13272b7310b25 } } From 89283fa3da03155fed5c8f8b7df8bd4e47ba0c27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 25 Nov 2022 04:08:01 +0100 Subject: [PATCH 40/47] rework PR comments --- backend/src/federation/index.ts | 10 ++-------- backend/src/index.ts | 6 +++++- deployment/bare_metal/.env.dist | 11 ++++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/backend/src/federation/index.ts b/backend/src/federation/index.ts index 352724274..82b961c63 100644 --- a/backend/src/federation/index.ts +++ b/backend/src/federation/index.ts @@ -10,15 +10,9 @@ function between(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1) + min) } -const getSeed = (): Buffer | null => { - if (CONFIG.FEDERATION_DHT_SEED) { - logger.debug(`with seed='${CONFIG.FEDERATION_DHT_SEED}'`) - return Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) - } - return null -} - const KEY_SECRET_SEEDBYTES = 32 +const getSeed = (): Buffer | null => + CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null const POLLTIME = 20000 const SUCCESSTIME = 120000 diff --git a/backend/src/index.ts b/backend/src/index.ts index 35fb191cd..e63f80827 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -21,7 +21,11 @@ async function main() { // start DHT hyperswarm when DHT_TOPIC is set in .env if (CONFIG.FEDERATION_DHT_TOPIC) { // eslint-disable-next-line no-console - console.log(`Federation active on ${CONFIG.FEDERATION_DHT_TOPIC}`) + console.log( + `starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${ + CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...' + }`, + ) await startDHT(CONFIG.FEDERATION_DHT_TOPIC) // con, } } diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 1d0e96455..5fb81c26a 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -26,7 +26,7 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code} COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" # backend -BACKEND_CONFIG_VERSION=v11.2022-10-27 +BACKEND_CONFIG_VERSION=v12.2022-11-10 JWT_EXPIRES_IN=10m GDT_API_URL=https://gdt.gradido.net @@ -59,10 +59,11 @@ WEBHOOK_ELOPAGE_SECRET=secret # EventProtocol EVENT_PROTOCOL_DISABLED=false -## DHT -## if you set this value, the DHT hyperswarm will start to announce and listen -## on an hash created from this tpoic -# DHT_TOPIC=GRADIDO_HUB +# Federation +# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen +# on an hash created from this topic +FEDERATION_DHT_TOPIC=GRADIDO_HUB +# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f # database DATABASE_CONFIG_VERSION=v1.2022-03-18 From 0f71a486a5e86d0be0dcbdd25c6f0c49c758e9c4 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 25 Nov 2022 14:13:22 +0100 Subject: [PATCH 41/47] log affected accounts & some fixes --- database/log/.gitignore | 2 + .../0054-recalculate_balance_and_decay.ts | 52 +++++++++++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 database/log/.gitignore diff --git a/database/log/.gitignore b/database/log/.gitignore new file mode 100644 index 000000000..c96a04f00 --- /dev/null +++ b/database/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/database/migrations/0054-recalculate_balance_and_decay.ts b/database/migrations/0054-recalculate_balance_and_decay.ts index fe9896db1..6614b4a52 100644 --- a/database/migrations/0054-recalculate_balance_and_decay.ts +++ b/database/migrations/0054-recalculate_balance_and_decay.ts @@ -13,6 +13,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import fs from 'fs' import Decimal from 'decimal.js-light' // Set precision value @@ -86,30 +87,63 @@ function calculateDecay( } export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // Write log file + const logFile = 'log/0054-recalculate_balance_and_decay.log.csv' + await fs.writeFile( + logFile, + `email;first_name;last_name;affected_transactions;new_balance;new_decay;old_balance;old_decay;delta;\n`, + (err) => { + if (err) throw err + }, + ) + // Find all users & loop over them const users = await queryFn('SELECT user_id FROM transactions GROUP BY user_id;') for (let u = 0; u < users.length; u++) { + const userId = users[u].user_id // find all transactions for a user const transactions = await queryFn( - `SELECT * FROM transactions WHERE user_id = ${users[u].user_id} ORDER BY balance_date ASC;`, + `SELECT *, CONVERT(balance, CHAR) as dec_balance, CONVERT(decay, CHAR) as dec_decay FROM transactions WHERE user_id = ${userId} ORDER BY balance_date ASC;`, ) + let previous = null + let affectedTransactions = 0 let balance = new Decimal(0) for (let t = 0; t < transactions.length; t++) { const transaction = transactions[t] - const decayStartDate = previous ? previous.balance_date : transaction.balance_date const amount = new Decimal(transaction.amount) const decay = calculateDecay(balance, decayStartDate, transaction.balance_date) balance = decay.balance.add(amount) - // Update - await queryFn(` - UPDATE transactions SET - balance = ${balance}, - decay = ${decay.decay ? decay.decay : 0} - WHERE id = ${transaction.id}; - `) + const userContact = await queryFn( + `SELECT email, first_name, last_name FROM users LEFT JOIN user_contacts ON users.email_id = user_contacts.id WHERE users.id = ${userId}`, + ) + const userEmail = userContact.length === 1 ? userContact[0].email : userId + const userFirstName = userContact.length === 1 ? userContact[0].first_name : '' + const userLastName = userContact.length === 1 ? userContact[0].last_name : '' + + // Update if needed + if (!balance.eq(transaction.dec_balance)) { + await queryFn(` + UPDATE transactions SET + balance = ${balance}, + decay = ${decay.decay ? decay.decay : 0} + WHERE id = ${transaction.id}; + `) + affectedTransactions++ + + // Log on last entry + if (t === transactions.length - 1) { + fs.appendFile( + logFile, + `${userEmail};${userFirstName};${userLastName};${affectedTransactions};${balance};${decay.decay ? decay.decay : 0};${transaction.dec_balance};${transaction.dec_decay};${balance.sub(transaction.dec_balance)};\n`, + (err) => { + if (err) throw err + }, + ) + } + } // previous previous = transaction From 1ea8eb1815b8da079da4b750e8c6519e4438c73b Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 25 Nov 2022 14:23:47 +0100 Subject: [PATCH 42/47] create log folder --- database/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/database/Dockerfile b/database/Dockerfile index 4069ffcd8..03c7d9a3b 100644 --- a/database/Dockerfile +++ b/database/Dockerfile @@ -100,6 +100,8 @@ COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json # Copy Mnemonic files COPY --from=build ${DOCKER_WORKDIR}/src/config/*.txt ./src/config/ +# Copy log folder +COPY --from=build ${DOCKER_WORKDIR}/log ./log # Copy run scripts run/ # COPY --from=build ${DOCKER_WORKDIR}/run ./run From bc0cbfe2f73d100807eaa6cc2b899a4c0f2eb891 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 25 Nov 2022 14:24:47 +0100 Subject: [PATCH 43/47] lint fix --- database/migrations/0054-recalculate_balance_and_decay.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/database/migrations/0054-recalculate_balance_and_decay.ts b/database/migrations/0054-recalculate_balance_and_decay.ts index 6614b4a52..516d0d1e3 100644 --- a/database/migrations/0054-recalculate_balance_and_decay.ts +++ b/database/migrations/0054-recalculate_balance_and_decay.ts @@ -105,7 +105,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis const transactions = await queryFn( `SELECT *, CONVERT(balance, CHAR) as dec_balance, CONVERT(decay, CHAR) as dec_decay FROM transactions WHERE user_id = ${userId} ORDER BY balance_date ASC;`, ) - + let previous = null let affectedTransactions = 0 let balance = new Decimal(0) @@ -137,7 +137,11 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis if (t === transactions.length - 1) { fs.appendFile( logFile, - `${userEmail};${userFirstName};${userLastName};${affectedTransactions};${balance};${decay.decay ? decay.decay : 0};${transaction.dec_balance};${transaction.dec_decay};${balance.sub(transaction.dec_balance)};\n`, + `${userEmail};${userFirstName};${userLastName};${affectedTransactions};${balance};${ + decay.decay ? decay.decay : 0 + };${transaction.dec_balance};${transaction.dec_decay};${balance.sub( + transaction.dec_balance, + )};\n`, (err) => { if (err) throw err }, From 62c48712a44f25b29b0768af5cb2a9ceeaed23f4 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 25 Nov 2022 14:32:12 +0100 Subject: [PATCH 44/47] fix misspelled text --- backend/src/graphql/resolver/TransactionResolver.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 44ccc838a..f4315d359 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -326,7 +326,7 @@ describe('send coins', () => { variables: { email: 'peter@lustig.de', amount: 50, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }, }), ).toEqual( @@ -342,7 +342,7 @@ describe('send coins', () => { // Find the exact transaction (sent one is the one with user[1] as user) const transaction = await Transaction.find({ userId: user[1].id, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( @@ -359,7 +359,7 @@ describe('send coins', () => { // Find the exact transaction (received one is the one with user[0] as user) const transaction = await Transaction.find({ userId: user[0].id, - memo: 'unrepeateable memo', + memo: 'unrepeatable memo', }) expect(EventProtocol.find()).resolves.toContainEqual( From 9253706513dbfd49e688915d5da01f138e131c31 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Fri, 25 Nov 2022 14:32:23 +0100 Subject: [PATCH 45/47] remove comment --- backend/src/util/validate.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/util/validate.ts b/backend/src/util/validate.ts index 8d1c90ca4..edd8d55f6 100644 --- a/backend/src/util/validate.ts +++ b/backend/src/util/validate.ts @@ -29,7 +29,6 @@ async function calculateBalance( const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) - // TODO why we have to use toString() here? const balance = decay.balance.add(amount.toString()) const transactionLinkRepository = getCustomRepository(TransactionLinkRepository) const { sumHoldAvailableAmount } = await transactionLinkRepository.summary(userId, time) From 2caf0ee664c57bf57fdf1957fdb00fb1b6ba2436 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Sat, 26 Nov 2022 01:14:24 +0100 Subject: [PATCH 46/47] v1.15.0 --- CHANGELOG.md | 13 +++++++++++++ admin/package.json | 2 +- backend/package.json | 2 +- database/package.json | 2 +- frontend/package.json | 2 +- package.json | 2 +- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ce354b1e..26b71ea03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.15.0](https://github.com/gradido/gradido/compare/1.14.1...1.15.0) + +- fix(database): wrong balance and decay values [`#2423`](https://github.com/gradido/gradido/pull/2423) +- fix(backend): wrong balance after transaction receive [`#2422`](https://github.com/gradido/gradido/pull/2422) +- feat(other): feature gradido roadmap [`#2301`](https://github.com/gradido/gradido/pull/2301) +- refactor(backend): new password encryption implementation [`#2353`](https://github.com/gradido/gradido/pull/2353) +- refactor(admin): statistics in a table and on separate page in admin area [`#2399`](https://github.com/gradido/gradido/pull/2399) +- feat(backend): 🍰 Email Templates [`#2163`](https://github.com/gradido/gradido/pull/2163) +- fix(backend): timezone problems [`#2393`](https://github.com/gradido/gradido/pull/2393) + #### [1.14.1](https://github.com/gradido/gradido/compare/1.14.0...1.14.1) +> 14 November 2022 + +- chore(release): version 1.14.1 - hotfix [`#2391`](https://github.com/gradido/gradido/pull/2391) - fix(frontend): load contributionMessages is fixed [`#2390`](https://github.com/gradido/gradido/pull/2390) #### [1.14.0](https://github.com/gradido/gradido/compare/1.13.3...1.14.0) diff --git a/admin/package.json b/admin/package.json index 7f0e7ffd5..75800a526 100644 --- a/admin/package.json +++ b/admin/package.json @@ -3,7 +3,7 @@ "description": "Administraion Interface for Gradido", "main": "index.js", "author": "Moriz Wahl", - "version": "1.14.1", + "version": "1.15.0", "license": "Apache-2.0", "private": false, "scripts": { diff --git a/backend/package.json b/backend/package.json index 3e26225bf..25774fc7d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "gradido-backend", - "version": "1.14.1", + "version": "1.15.0", "description": "Gradido unified backend providing an API-Service for Gradido Transactions", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/backend", diff --git a/database/package.json b/database/package.json index 6216a25fb..abc7789c4 100644 --- a/database/package.json +++ b/database/package.json @@ -1,6 +1,6 @@ { "name": "gradido-database", - "version": "1.14.1", + "version": "1.15.0", "description": "Gradido Database Tool to execute database migrations", "main": "src/index.ts", "repository": "https://github.com/gradido/gradido/database", diff --git a/frontend/package.json b/frontend/package.json index cfc12630e..6f1474521 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "bootstrap-vue-gradido-wallet", - "version": "1.14.1", + "version": "1.15.0", "private": true, "scripts": { "start": "node run/server.js", diff --git a/package.json b/package.json index 72efee984..22f444155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gradido", - "version": "1.14.1", + "version": "1.15.0", "description": "Gradido", "main": "index.js", "repository": "git@github.com:gradido/gradido.git", From 0d9b759bead5a8cbf12d57abfdc3cea849606ded Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:09:00 +0100 Subject: [PATCH 47/47] Update deployment/bare_metal/.env.dist set federation diabled as default Co-authored-by: Ulf Gebhardt --- deployment/bare_metal/.env.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index 5fb81c26a..9c6bfd735 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -62,7 +62,7 @@ EVENT_PROTOCOL_DISABLED=false # Federation # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # on an hash created from this topic -FEDERATION_DHT_TOPIC=GRADIDO_HUB +# FEDERATION_DHT_TOPIC=GRADIDO_HUB # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f # database