From 9b37ac9e8b79921ab87b620436484ea012a32142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 15 Jul 2022 02:49:34 +0200 Subject: [PATCH 01/92] some small updates --- .../UC_Introduction_of_Gradido-ID.md | 286 +++++++++--------- 1 file changed, 146 insertions(+), 140 deletions(-) diff --git a/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md b/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md index e3c0ac2d7..5da969eac 100644 --- a/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md +++ b/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md @@ -1,140 +1,146 @@ -# Introduction of Gradido-ID - -## Motivation - -To introduce the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage. - -Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account. - -## Definition - -The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID). - -## Steps of Introduction - -To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all. - -The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID. - -### Database-Schema - -#### Users-Table - -The entity users has to be changed by adding the following columns. - -| Column | Type | Description | -| ------------------------ | ------ | -------------------------------------------------------------------------------------- | -| gradidoID | String | technical unique key of the user as UUID (version 4) | -| alias | String | a business unique key of the user | -| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... | -| emailID | int | technical foreign key to the new entity Contact | - -##### Email vs emailID - -The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table. - -The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table. - -#### new UserContacts-Table - -A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses. - -| Column | Type | Description | -| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | int | the technical key of a contact entity | -| type | int | Defines the type of contact entry as enum: Email, Phone, etc | -| usersID | int | Defines the foreign key to the `Users` table | -| email | String | defines the address of a contact entry of type Email | -| phone | String | defines the address of a contact entry of type Phone | -| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... | - -### Database-Migration - -After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables. - -#### Initialize GradidoID - -In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4). - -#### Primary Email Contact - -In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email` and create for it a new entry in the `UsersContact `table, by initializing the contact-values with: - -* id = new technical key -* type = Enum-Email -* userID = `Users.id` -* email = `Users.email` -* phone = null -* usedChannel = Enum-"main contact" - -and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1` - -After this one-time migration the column `Users.email` can be deleted. - -### Adaption of BusinessLogic - -The following logic or business processes has to be adapted for introducing the Gradido-ID - -#### Read-Write Access of Users-Table especially Email - -The ORM mapping has to be adapted to the changed and new database schema. - -#### Registration Process - -The logic of the registration process has to be adapted by - -* initializing the `Users.userID` with a unique UUID -* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel ` -* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry -* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email` - -#### Login Process - -The logic of the login process has to be adapted by - -* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input -* depending on the `Users.passphraseEncryptionType` decrypt the stored password - * = 1 : with the email - * = 2 : with the userID - -#### Password En/Decryption - -The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID. - -#### Change Password Process - -The logic of change password has to be adapted by - -* if the `Users.passphraseEncryptionType` = 1, then - - * read the users email address from the `UsersContact `table - * give the email address as input for the password decryption of the existing password - * use the `Users.userID` as input for the password encryption fo the new password - * change the `Users.passphraseEnrycptionType` to the new value =2 -* if the `Users.passphraseEncryptionType` = 2, then - - * give the `Users.userID` as input for the password decryption of the existing password - * use the `Users.userID` as input for the password encryption fo the new password - -#### Search- and Access Logic - -A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by - -* searching per email (only with maincontact as contactchannel) -* searching per userID -* searching per alias - -#### Identity-Mapping - -A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output: - -* email -> userID -* email -> alias -* userID -> email -* userID -> alias -* alias -> email -* alias -> userID - -#### GDT-Access - -To use the GDT-servers api the used identifier for GDT has to be switch from email to userID. +# Introduction of Gradido-ID + +## Motivation + +The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage. + +Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account. + +## Definition + +The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID). + +## Steps of Introduction + +To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all. + +The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID. + +### Database-Schema + +#### Users-Table + +The entity users has to be changed by adding the following columns. + +| Column | Type | Description | +| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- | +| gradidoID | String | technical unique key of the user as UUID (version 4) | +| alias | String | a business unique key of the user | +| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... | +| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts | + +##### Email vs emailID + +The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table. + +The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table. + +#### new UserContacts-Table + +A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses. + +| Column | Type | Description | +| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | int | the technical key of a contact entity | +| type | int | Defines the type of contact entry as enum: Email, Phone, etc | +| usersID | int | Defines the foreign key to the `Users` table | +| email | String | defines the address of a contact entry of type Email | +| phone | String | defines the address of a contact entry of type Phone | +| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... | + +### Database-Migration + +After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables. + +#### Initialize GradidoID + +In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4). + +#### Primary Email Contact + +In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email` and create for it a new entry in the `UsersContact `table, by initializing the contact-values with: + +* id = new technical key +* type = Enum-Email +* userID = `Users.id` +* email = `Users.email` +* phone = null +* usedChannel = Enum-"main contact" + +and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1` + +After this one-time migration the column `Users.email` can be deleted. + +### Adaption of BusinessLogic + +The following logic or business processes has to be adapted for introducing the Gradido-ID + +#### Read-Write Access of Users-Table especially Email + +The ORM mapping has to be adapted to the changed and new database schema. + +#### Registration Process + +The logic of the registration process has to be adapted by + +* initializing the `Users.userID` with a unique UUID +* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel ` +* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry +* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email` + +#### Login Process + +The logic of the login process has to be adapted by + +* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input +* depending on the `Users.passphraseEncryptionType` decrypt the stored password + * = 1 : with the email + * = 2 : with the userID + +#### Password En/Decryption + +The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID. + +#### Change Password Process + +The logic of change password has to be adapted by + +* if the `Users.passphraseEncryptionType` = 1, then + + * read the users email address from the `UsersContact `table + * give the email address as input for the password decryption of the existing password + * use the `Users.userID` as input for the password encryption for the new password + * change the `Users.passphraseEnrycptionType` to the new value =2 +* if the `Users.passphraseEncryptionType` = 2, then + + * give the `Users.userID` as input for the password decryption of the existing password + * use the `Users.userID` as input for the password encryption fo the new password + +#### Search- and Access Logic + +A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by + +* searching per email (only with maincontact as contactchannel) +* searching per userID +* searching per alias + +#### Identity-Mapping + +A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output: + +* email -> userID +* email -> gradidoID +* email -> alias +* userID -> gradidoID +* userID -> email +* userID -> alias +* alias -> gradidoID +* alias -> email +* alias -> userID +* gradidoID -> email +* gradidoID -> userID +* gradidoID -> alias + +#### GDT-Access + +To use the GDT-servers api the used identifier for GDT has to be switch from email to userID. From 7eb1f1ad74c1473b5012034c39595d432a9e28da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 15 Jul 2022 03:40:26 +0200 Subject: [PATCH 02/92] add database migrations --- .../0044-adapt_users_table_for_gradidoid.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 database/migrations/0044-adapt_users_table_for_gradidoid.ts diff --git a/database/migrations/0044-adapt_users_table_for_gradidoid.ts b/database/migrations/0044-adapt_users_table_for_gradidoid.ts new file mode 100644 index 000000000..29fae353e --- /dev/null +++ b/database/migrations/0044-adapt_users_table_for_gradidoid.ts @@ -0,0 +1,42 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds new columns to the table `users` and creates the + * new table `user_contacts` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE IF NOT EXISTS \`user_contacts\` ( + \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, + \`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + \`users_id\` int(10) unsigned NOT NULL, + \`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL UNIQUE, + \`email_hash\` binary(32) NULL, + \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, + \`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` datetime NULL DEFAULT NULL, + \`deleted_at\` datetime NULL DEFAULT NULL, + PRIMARY KEY (\`id\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) + + await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` varchar(36) NULL AFTER `id`;') + await queryFn('ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL AFTER `gradido_id`;') + await queryFn( + 'ALTER TABLE `users` ADD COLUMN `passphrase_encrypt_type` varchar(36) NULL AFTER `privkey`;', + ) + await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // write downgrade logic as parameter of queryFn + await queryFn(`DROP TABLE IF EXISTS \`user_contacts\`;`) + + await queryFn('ALTER TABLE `users` DROP COLUMN `gradido_id`;') + await queryFn('ALTER TABLE `users` DROP COLUMN `alias`;') + await queryFn('ALTER TABLE `users` DROP COLUMN `passphrase_encrypt_type`;') + await queryFn('ALTER TABLE `users` DROP COLUMN `email_id`;') +} From 4e9e834df497526b5a81f5e8d0e346114ab2f476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 15 Jul 2022 16:37:32 +0200 Subject: [PATCH 03/92] adapt users table --- .../User.ts | 109 ++++++++++++++++++ database/entity/User.ts | 2 +- database/entity/index.ts | 2 + .../0044-adapt_users_table_for_gradidoid.ts | 2 +- 4 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 database/entity/0044-adapt_users_table_for_gradidoid/User.ts diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts new file mode 100644 index 000000000..658638b5e --- /dev/null +++ b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts @@ -0,0 +1,109 @@ +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' + +@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: true, + default: null, + 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({ + name: 'passphrase_encrypt_type', + length: 36, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + passphraseEncryptType: string + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) + emailId?: number | null + + @Column({ + name: 'first_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + firstName: string + + @Column({ + name: 'last_name', + length: 255, + nullable: true, + default: null, + collation: 'utf8mb4_unicode_ci', + }) + lastName: string + + @DeleteDateColumn() + deletedAt: Date | null + + @Column({ type: 'bigint', default: 0, unsigned: true }) + password: BigInt + + @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) + emailHash: Buffer + + @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) + emailChecked: boolean + + @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) + language: string + + @Column({ name: 'is_admin', type: 'datetime', nullable: true, default: null }) + isAdmin: Date | null + + @Column({ name: 'referrer_id', type: 'int', unsigned: true, nullable: true, default: null }) + referrerId?: number | null + + @Column({ + name: 'contribution_link_id', + type: 'int', + unsigned: true, + nullable: true, + default: null, + }) + contributionLinkId?: number | null + + @Column({ name: 'publisher_id', default: 0 }) + publisherId: number + + @Column({ + type: 'text', + name: 'passphrase', + collation: 'utf8mb4_unicode_ci', + nullable: true, + default: null, + }) + passphrase: string +} diff --git a/database/entity/User.ts b/database/entity/User.ts index 99b8c8ca9..a29e87cd7 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0040-add_contribution_link_id_to_user/User' +export { User } from './0044-adapt_users_table_for_gradidoid/User' diff --git a/database/entity/index.ts b/database/entity/index.ts index 266c40740..76acba3fa 100644 --- a/database/entity/index.ts +++ b/database/entity/index.ts @@ -5,6 +5,7 @@ import { Migration } from './Migration' import { Transaction } from './Transaction' import { TransactionLink } from './TransactionLink' import { User } from './User' +import { UserContact } from './UserContact' import { Contribution } from './Contribution' export const entities = [ @@ -16,4 +17,5 @@ export const entities = [ Transaction, TransactionLink, User, + UserContact, ] diff --git a/database/migrations/0044-adapt_users_table_for_gradidoid.ts b/database/migrations/0044-adapt_users_table_for_gradidoid.ts index 29fae353e..eb5f8e2cf 100644 --- a/database/migrations/0044-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0044-adapt_users_table_for_gradidoid.ts @@ -1,6 +1,6 @@ /* MIGRATION TO ADD GRADIDO_ID * - * This migration adds new columns to the table `users` and creates the + * This migration adds new columns to the table `users` and creates the * new table `user_contacts` */ From 138891dfe36ad44f5375cabe388995195850f2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 15 Jul 2022 16:37:49 +0200 Subject: [PATCH 04/92] add user_contacts table --- .../UserContact.ts | 40 +++++++++++++++++++ database/entity/UserContact.ts | 1 + 2 files changed, 41 insertions(+) create mode 100644 database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts create mode 100644 database/entity/UserContact.ts diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts b/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts new file mode 100644 index 000000000..41f622722 --- /dev/null +++ b/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts @@ -0,0 +1,40 @@ +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' + +@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 + + @Column({ name: 'users_id', type: 'int', unsigned: true, nullable: false }) + usersId?: number | null + + @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) + email: string + + @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', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + createdAt: Date + + @DeleteDateColumn() + updatedAt: Date | null + + @DeleteDateColumn() + deletedAt: Date | null +} diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts new file mode 100644 index 000000000..dce775516 --- /dev/null +++ b/database/entity/UserContact.ts @@ -0,0 +1 @@ +export { UserContact } from './0044-adapt_users_table_for_gradidoid/UserContact' From 5d15ea7d7afe50914e97eb2d74a2b8fa6b324ff4 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 19 Jul 2022 02:45:01 +0200 Subject: [PATCH 05/92] Update database/entity/0044-adapt_users_table_for_gradidoid/User.ts Co-authored-by: Moriz Wahl --- database/entity/0044-adapt_users_table_for_gradidoid/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts index 658638b5e..ca6d80cb9 100644 --- a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts +++ b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts @@ -8,7 +8,7 @@ export class User extends BaseEntity { @Column({ name: 'gradido_id', length: 36, - nullable: true, + nullable: false, default: null, collation: 'utf8mb4_unicode_ci', }) From 950241a2f0696a3b0b3828766608e4f73ca0e5a9 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 19 Jul 2022 02:46:06 +0200 Subject: [PATCH 06/92] Update database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts Co-authored-by: Moriz Wahl --- .../0044-adapt_users_table_for_gradidoid/UserContact.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts b/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts index 41f622722..53aac52ab 100644 --- a/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts +++ b/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts @@ -14,8 +14,8 @@ export class UserContact extends BaseEntity { }) type: string - @Column({ name: 'users_id', type: 'int', unsigned: true, nullable: false }) - usersId?: number | null + @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 From 3a9186d192e64f85cabd1d8f6a4f7233d5d2d33e Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 19 Jul 2022 02:47:17 +0200 Subject: [PATCH 07/92] Update database/migrations/0044-adapt_users_table_for_gradidoid.ts Co-authored-by: Moriz Wahl --- database/migrations/0044-adapt_users_table_for_gradidoid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/0044-adapt_users_table_for_gradidoid.ts b/database/migrations/0044-adapt_users_table_for_gradidoid.ts index eb5f8e2cf..4ef06e715 100644 --- a/database/migrations/0044-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0044-adapt_users_table_for_gradidoid.ts @@ -12,7 +12,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis CREATE TABLE IF NOT EXISTS \`user_contacts\` ( \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, \`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - \`users_id\` int(10) unsigned NOT NULL, + \`user_id\` int(10) unsigned NOT NULL, \`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL UNIQUE, \`email_hash\` binary(32) NULL, \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, From c02e12fa969412defd0a9d5fa9bf497c539db45d Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 19 Jul 2022 02:47:36 +0200 Subject: [PATCH 08/92] Update database/migrations/0044-adapt_users_table_for_gradidoid.ts Co-authored-by: Moriz Wahl --- database/migrations/0044-adapt_users_table_for_gradidoid.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/0044-adapt_users_table_for_gradidoid.ts b/database/migrations/0044-adapt_users_table_for_gradidoid.ts index 4ef06e715..f1500c8c1 100644 --- a/database/migrations/0044-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0044-adapt_users_table_for_gradidoid.ts @@ -23,7 +23,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` varchar(36) NULL AFTER `id`;') + await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` UUID NOT NULL DEFAULT UUID() AFTER `id`;') await queryFn('ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL AFTER `gradido_id`;') await queryFn( 'ALTER TABLE `users` ADD COLUMN `passphrase_encrypt_type` varchar(36) NULL AFTER `privkey`;', From 126495676f0956f814f5141f2ec000a7f1f5ffb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 19 Jul 2022 02:51:37 +0200 Subject: [PATCH 09/92] upgrade DB_VERSION to 0044 --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 2120fce71..101d2d702 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0043-add_event_protocol_table', + DB_VERSION: '0044-adapt_users_table_for_gradidoid', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info From f125f967f2909f9c6338e8e89b57c6f5eaf2507b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 19 Jul 2022 02:52:54 +0200 Subject: [PATCH 10/92] delete columns passphraseEncryptType and emailHash --- .../0044-adapt_users_table_for_gradidoid/User.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts index 658638b5e..c6d26524a 100644 --- a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts +++ b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts @@ -29,15 +29,6 @@ export class User extends BaseEntity { @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) privKey: Buffer - @Column({ - name: 'passphrase_encrypt_type', - length: 36, - nullable: true, - default: null, - collation: 'utf8mb4_unicode_ci', - }) - passphraseEncryptType: string - @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) email: string @@ -68,9 +59,6 @@ export class User extends BaseEntity { @Column({ type: 'bigint', default: 0, unsigned: true }) password: BigInt - @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) - emailHash: Buffer - @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) createdAt: Date From 0e2bb874372532cf20cb7a181ddecf8219018cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 19 Jul 2022 23:36:59 +0200 Subject: [PATCH 11/92] return migration back to 0039 --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 101d2d702..6340fb697 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0044-adapt_users_table_for_gradidoid', + DB_VERSION: '0039-contributions_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info From ab765e7a8f223abab45186bb351b0396ee9a31d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 19 Jul 2022 23:51:38 +0200 Subject: [PATCH 12/92] shift User from migration 0044 to 0039 --- database/entity/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/entity/User.ts b/database/entity/User.ts index a29e87cd7..2226e4d51 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0044-adapt_users_table_for_gradidoid/User' +export { User } from './0039-contributions_table/User' From cca6226b55cc4bf72350121141686f532098534c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 21 Jul 2022 23:15:29 +0200 Subject: [PATCH 13/92] new table user_contacts and change table users --- .../migrations/0044-adapt_users_table_for_gradidoid.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/migrations/0044-adapt_users_table_for_gradidoid.ts b/database/migrations/0044-adapt_users_table_for_gradidoid.ts index f1500c8c1..7b9cdbc94 100644 --- a/database/migrations/0044-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0044-adapt_users_table_for_gradidoid.ts @@ -23,10 +23,11 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` UUID NOT NULL DEFAULT UUID() AFTER `id`;') - await queryFn('ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL AFTER `gradido_id`;') await queryFn( - 'ALTER TABLE `users` ADD COLUMN `passphrase_encrypt_type` varchar(36) NULL AFTER `privkey`;', + 'ALTER TABLE `users` ADD COLUMN `gradido_id` varchar(36) NOT NULL UNIQUE DEFAULT UUID() AFTER `id`;', + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', ) await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') } @@ -37,6 +38,5 @@ export async function downgrade(queryFn: (query: string, values?: any[]) => Prom await queryFn('ALTER TABLE `users` DROP COLUMN `gradido_id`;') await queryFn('ALTER TABLE `users` DROP COLUMN `alias`;') - await queryFn('ALTER TABLE `users` DROP COLUMN `passphrase_encrypt_type`;') await queryFn('ALTER TABLE `users` DROP COLUMN `email_id`;') } From cb4ee4590ad5ed68a7408a1fefe3d0b79178ed52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 21 Jul 2022 23:16:19 +0200 Subject: [PATCH 14/92] adapt User and UserContact entity --- .../0044-adapt_users_table_for_gradidoid/User.ts | 16 ++++++++++++++-- .../UserContact.ts | 6 +++--- database/entity/User.ts | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts index f4063f65c..1e7e9d8d8 100644 --- a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts +++ b/database/entity/0044-adapt_users_table_for_gradidoid/User.ts @@ -1,4 +1,13 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToMany, + JoinColumn, +} from 'typeorm' +import { Contribution } from '../Contribution' @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class User extends BaseEntity { @@ -9,7 +18,6 @@ export class User extends BaseEntity { name: 'gradido_id', length: 36, nullable: false, - default: null, collation: 'utf8mb4_unicode_ci', }) gradidoID: string @@ -94,4 +102,8 @@ export class User extends BaseEntity { default: null, }) passphrase: string + + @OneToMany(() => Contribution, (contribution) => contribution.user) + @JoinColumn({ name: 'user_id' }) + contributions?: Contribution[] } diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts b/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts index 53aac52ab..fee0afeda 100644 --- a/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts +++ b/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts @@ -29,12 +29,12 @@ export class UserContact extends BaseEntity { @Column({ length: 255, unique: false, nullable: true, collation: 'utf8mb4_unicode_ci' }) phone: string - @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) + @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) createdAt: Date - @DeleteDateColumn() + @DeleteDateColumn({ name: 'updated_at', nullable: true }) updatedAt: Date | null - @DeleteDateColumn() + @DeleteDateColumn({ name: 'deleted_at', nullable: true }) deletedAt: Date | null } diff --git a/database/entity/User.ts b/database/entity/User.ts index 2226e4d51..a29e87cd7 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0039-contributions_table/User' +export { User } from './0044-adapt_users_table_for_gradidoid/User' From 26985ef49c7eac8fd0161a6650ef470673086c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 21 Jul 2022 23:17:05 +0200 Subject: [PATCH 15/92] switch DB_VERSION to 0044 --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 6340fb697..101d2d702 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0039-contributions_table', + DB_VERSION: '0044-adapt_users_table_for_gradidoid', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info From 86a0ab731fb18e9735f9929f1e969396e11242db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 21 Jul 2022 23:18:31 +0200 Subject: [PATCH 16/92] adapt UserResolver and Test to database changes --- backend/src/graphql/resolver/UserResolver.test.ts | 5 ++++- backend/src/graphql/resolver/UserResolver.ts | 4 ++-- backend/src/util/communityUser.ts | 4 +++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index a2a499224..9034df8f6 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -111,13 +111,16 @@ describe('UserResolver', () => { expect(user).toEqual([ { id: expect.any(Number), + gradidoID: expect.any(String), + alias: null, email: 'peter@lustig.de', + emailId: null, firstName: 'Peter', lastName: 'Lustig', password: '0', pubKey: null, privKey: null, - emailHash: expect.any(Buffer), + // emailHash: expect.any(Buffer), createdAt: expect.any(Date), emailChecked: false, passphrase: expect.any(String), diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index a89a8cb0b..f61414e42 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -377,7 +377,7 @@ export class UserResolver { // const keyPair = KeyPairEd25519Create(passphrase) // return pub, priv Key // const passwordHash = SecretKeyCryptographyCreateKey(email, password) // return short and long hash // const encryptedPrivkey = SecretKeyCryptographyEncrypt(keyPair[1], passwordHash[1]) - const emailHash = getEmailHash(email) + // const emailHash = getEmailHash(email) const eventRegister = new EventRegister() const eventRedeemRegister = new EventRedeemRegister() @@ -386,7 +386,7 @@ export class UserResolver { dbUser.email = email dbUser.firstName = firstName dbUser.lastName = lastName - dbUser.emailHash = emailHash + // dbUser.emailHash = emailHash dbUser.language = language dbUser.publisherId = publisherId dbUser.passphrase = passphrase.join(' ') diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 1a84c2cdf..c90e786c6 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -6,6 +6,8 @@ import { User } from '@model/User' const communityDbUser: dbUser = { id: -1, + gradidoID: '11111111-2222-3333-4444-55555555', + alias: '', email: 'support@gradido.net', firstName: 'Gradido', lastName: 'Akademie', @@ -13,7 +15,7 @@ const communityDbUser: dbUser = { privKey: Buffer.from(''), deletedAt: null, password: BigInt(0), - emailHash: Buffer.from(''), + // emailHash: Buffer.from(''), createdAt: new Date(), emailChecked: false, language: '', From afd123e133c5f02e4628f3f84fbd5eee13b7373c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 27 Jul 2022 01:09:55 +0200 Subject: [PATCH 17/92] correct the merge-conflict solving --- docker-compose.override.yml | 124 ------------------------------------ 1 file changed, 124 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index fbd0cd734..f8fde0430 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,129 +1,5 @@ version: "3.4" -services: - - ######################################################## - # FRONTEND ############################################# - ######################################################## - frontend: - # name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there - image: gradido/frontend:local-development - build: - target: development - environment: - - NODE_ENV="development" - # - DEBUG=true - volumes: - # This makes sure the docker container has its own node modules. - # Therefore it is possible to have a different node version on the host machine - - frontend_node_modules:/app/node_modules - # bind the local folder to the docker to allow live reload - - ./frontend:/app - - ######################################################## - # ADMIN INTERFACE ###################################### - ######################################################## - admin: - # name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there - image: gradido/admin:local-development - build: - target: development - environment: - - NODE_ENV="development" - # - DEBUG=true - volumes: - # This makes sure the docker container has its own node modules. - # Therefore it is possible to have a different node version on the host machine - - admin_node_modules:/app/node_modules - # bind the local folder to the docker to allow live reload - - ./admin:/app - - ######################################################## - # BACKEND ############################################## - ######################################################## - backend: - # name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there - image: gradido/backend:local-development - build: - target: development - networks: - - external-net - - internal-net - environment: - - NODE_ENV="development" - volumes: - # This makes sure the docker container has its own node modules. - # Therefore it is possible to have a different node version on the host machine - - backend_node_modules:/app/node_modules - - backend_database_node_modules:/database/node_modules - - backend_database_build:/database/build - # bind the local folder to the docker to allow live reload - - ./backend:/app - - ./database:/database - - ######################################################## - # DATABASE ############################################## - ######################################################## - database: - # we always run on production here since else the service lingers - # feel free to change this behaviour if it seems useful - # Due to problems with the volume caching the built files - # we changed this to test build. This keeps the service running. - # name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there - image: gradido/database:local-test_up - build: - target: test_up - environment: - - NODE_ENV="development" - volumes: - # This makes sure the docker container has its own node modules. - # Therefore it is possible to have a different node version on the host machine - - database_node_modules:/app/node_modules - - database_build:/app/build - # bind the local folder to the docker to allow live reload - - ./database:/app - - ######################################################### - ## MARIADB ############################################## - ######################################################### - mariadb: - networks: - - internal-net - - external-net - - ######################################################### - ## NGINX ################################################ - ######################################################### - nginx: - volumes: - - ./logs/nginx:/var/log/nginx - - ######################################################### - ## PHPMYADMIN ########################################### - ######################################################### - phpmyadmin: - image: phpmyadmin - environment: - - PMA_ARBITRARY=1 - #restart: always - ports: - - 8074:80 - networks: - - internal-net - - external-net - volumes: - - /sessions - -volumes: - frontend_node_modules: - admin_node_modules: - backend_node_modules: - backend_database_node_modules: - backend_database_build: - database_node_modules: - database_build: -version: "3.4" - services: ######################################################## From 0aa51115780cc296d9ac2d0944cf9c5409786492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 2 Aug 2022 02:34:16 +0200 Subject: [PATCH 18/92] switch 0044 to 0045 Migration --- .../User.ts | 0 .../UserContact.ts | 0 database/entity/User.ts | 2 +- database/entity/UserContact.ts | 2 +- .../0044-adapt_users_table_for_gradidoid.ts | 42 --------- .../0045-adapt_users_table_for_gradidoid.ts | 90 +++++++++++++++++++ .../UC_Introduction_of_Gradido-ID.md | 33 ++++--- 7 files changed, 115 insertions(+), 54 deletions(-) rename database/entity/{0044-adapt_users_table_for_gradidoid => 0045-adapt_users_table_for_gradidoid}/User.ts (100%) rename database/entity/{0044-adapt_users_table_for_gradidoid => 0045-adapt_users_table_for_gradidoid}/UserContact.ts (100%) delete mode 100644 database/migrations/0044-adapt_users_table_for_gradidoid.ts create mode 100644 database/migrations/0045-adapt_users_table_for_gradidoid.ts diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/User.ts b/database/entity/0045-adapt_users_table_for_gradidoid/User.ts similarity index 100% rename from database/entity/0044-adapt_users_table_for_gradidoid/User.ts rename to database/entity/0045-adapt_users_table_for_gradidoid/User.ts diff --git a/database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts b/database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts similarity index 100% rename from database/entity/0044-adapt_users_table_for_gradidoid/UserContact.ts rename to database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts diff --git a/database/entity/User.ts b/database/entity/User.ts index a29e87cd7..89b5d3d7f 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0044-adapt_users_table_for_gradidoid/User' +export { User } from './0045-adapt_users_table_for_gradidoid/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index dce775516..ac47fac24 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0044-adapt_users_table_for_gradidoid/UserContact' +export { UserContact } from './0045-adapt_users_table_for_gradidoid/UserContact' diff --git a/database/migrations/0044-adapt_users_table_for_gradidoid.ts b/database/migrations/0044-adapt_users_table_for_gradidoid.ts deleted file mode 100644 index 7b9cdbc94..000000000 --- a/database/migrations/0044-adapt_users_table_for_gradidoid.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* MIGRATION TO ADD GRADIDO_ID - * - * This migration adds new columns to the table `users` and creates the - * new table `user_contacts` - */ - -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { - await queryFn(` - CREATE TABLE IF NOT EXISTS \`user_contacts\` ( - \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, - \`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, - \`user_id\` int(10) unsigned NOT NULL, - \`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL UNIQUE, - \`email_hash\` binary(32) NULL, - \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, - \`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, - \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - \`updated_at\` datetime NULL DEFAULT NULL, - \`deleted_at\` datetime NULL DEFAULT NULL, - PRIMARY KEY (\`id\`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - - await queryFn( - 'ALTER TABLE `users` ADD COLUMN `gradido_id` varchar(36) NOT NULL UNIQUE DEFAULT UUID() AFTER `id`;', - ) - await queryFn( - 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', - ) - await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') -} - -export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { - // write downgrade logic as parameter of queryFn - await queryFn(`DROP TABLE IF EXISTS \`user_contacts\`;`) - - await queryFn('ALTER TABLE `users` DROP COLUMN `gradido_id`;') - await queryFn('ALTER TABLE `users` DROP COLUMN `alias`;') - await queryFn('ALTER TABLE `users` DROP COLUMN `email_id`;') -} diff --git a/database/migrations/0045-adapt_users_table_for_gradidoid.ts b/database/migrations/0045-adapt_users_table_for_gradidoid.ts new file mode 100644 index 000000000..65c2d4b97 --- /dev/null +++ b/database/migrations/0045-adapt_users_table_for_gradidoid.ts @@ -0,0 +1,90 @@ +/* MIGRATION TO ADD GRADIDO_ID + * + * This migration adds new columns to the table `users` and creates the + * new table `user_contacts` + */ + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE FUNCTION UuidToBin(_uuid BINARY(36)) + RETURNS BINARY(16) + LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER + RETURN + UNHEX(CONCAT( + SUBSTR(_uuid, 15, 4), + SUBSTR(_uuid, 10, 4), + SUBSTR(_uuid, 1, 8), + SUBSTR(_uuid, 20, 4), + SUBSTR(_uuid, 25) ));`) + + await queryFn(` + CREATE FUNCTION UuidFromBin(_bin BINARY(16)) + RETURNS BINARY(36) + LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER + RETURN + LCASE(CONCAT_WS('-', + HEX(SUBSTR(_bin, 5, 4)), + HEX(SUBSTR(_bin, 3, 2)), + HEX(SUBSTR(_bin, 1, 2)), + HEX(SUBSTR(_bin, 9, 2)), + HEX(SUBSTR(_bin, 11)) + ));`) + + await queryFn(` + CREATE TABLE IF NOT EXISTS \`user_contacts\` ( + \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, + \`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + \`user_id\` int(10) unsigned NOT NULL, + \`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + \`email_verification_code\` bigint(20) unsigned NOT NULL, + \`email_opt_in_type_id\` int NOT NULL, + \`email_resend_count\` int DEFAULT '0', + \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, + \`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updated_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + \`deleted_at\` datetime NULL DEFAULT NULL, + PRIMARY KEY (\`id\`), + UNIQUE KEY \`email_verification_code\` (\`email_verification_code\`), + UNIQUE KEY \`email\` (\`email\`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) + + await queryFn( + 'ALTER TABLE `users` ADD COLUMN `gradido_id` BINARY(16) NOT NULL UNIQUE DEFAULT UuidToBin(UUID()) AFTER `id`;', + ) + await queryFn( + 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', + ) + await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') + await queryFn(` + INSERT INTO gradido_community.user_contacts + (type, user_id, email, email_verification_code, email_opt_in_type_id, email_resent_count, email_checked, created_at, updated_at, deleted_at) + SELECT + 'EMAIL' as type, + u.id as user_id, + u.email, + e.verification_code, + e.email_opt_in_type_id, + e.resend_count, + u.email_checked, + e.created, + e.updated, + u.deletedAt + FROM + gradido_community.users as u, + gradido_community.login_email_opt_in as e + WHERE + u.id = e.user_id;`) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // write downgrade logic as parameter of queryFn + await queryFn(`DROP TABLE IF EXISTS \`user_contacts\`;`) + + await queryFn('ALTER TABLE `users` DROP COLUMN `gradido_id`;') + await queryFn('ALTER TABLE `users` DROP COLUMN `alias`;') + await queryFn('ALTER TABLE `users` DROP COLUMN `email_id`;') +} diff --git a/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md b/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md index 5da969eac..c8eb12524 100644 --- a/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md +++ b/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md @@ -39,14 +39,21 @@ The preferred and proper solution will be to add a new column `Users.emailId `as A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses. -| Column | Type | Description | -| --------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | int | the technical key of a contact entity | -| type | int | Defines the type of contact entry as enum: Email, Phone, etc | -| usersID | int | Defines the foreign key to the `Users` table | -| email | String | defines the address of a contact entry of type Email | -| phone | String | defines the address of a contact entry of type Phone | -| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... | +| Column | Type | Description | +| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | int | the technical key of a contact entity | +| type | int | Defines the type of contact entry as enum: Email, Phone, etc | +| userID | int | Defines the foreign key to the `Users` table | +| email | String | defines the address of a contact entry of type Email | +| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset | +| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 | +| emailResendCount | int | counter how often the email was resend | +| emailChecked | boolean | flag if email is verified and confirmed | +| createdAt | DateTime | point of time the Contact was created | +| updatedAt | DateTime | point of time the Contact was updated | +| deletedAt | DateTime | point of time the Contact was soft deleted | +| phone | String | defines the address of a contact entry of type Phone | +| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... | ### Database-Migration @@ -58,18 +65,24 @@ In a one-time migration create for each entry of the `Users `tabel an unique UUI #### Primary Email Contact -In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email` and create for it a new entry in the `UsersContact `table, by initializing the contact-values with: +In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with: * id = new technical key * type = Enum-Email * userID = `Users.id` * email = `Users.email` +* emailVerifyCode = `login_email_opt_in.verification_code` +* emailOptInType = `login_email_opt_in.email_opt_in_type_id` +* emailResendCount = `login_email_opt_in.resent_count` +* emailChecked = `Users.emailChecked` +* createdAt = `login_email_opt_in.created_at` +* updatedAt = `login_email_opt_in.updated_at` * phone = null * usedChannel = Enum-"main contact" and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1` -After this one-time migration the column `Users.email` can be deleted. +After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted. ### Adaption of BusinessLogic From f959182660077f47b0216f5fb55bf9bc6b1a48e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 5 Aug 2022 03:31:13 +0200 Subject: [PATCH 19/92] add package uuid --- package.json | 3 ++- yarn.lock | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index bf8eced01..fca1994b3 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "release": "scripts/release.sh" }, "dependencies": { - "auto-changelog": "^2.4.0" + "auto-changelog": "^2.4.0", + "uuid": "^8.3.2" } } diff --git a/yarn.lock b/yarn.lock index d9c16e6f7..2c8f9b681 100644 --- a/yarn.lock +++ b/yarn.lock @@ -81,6 +81,11 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.13.1.tgz#2749d4b8b5b7d67460b4a418023ff73c3fefa60a" integrity sha512-EWhx3fHy3M9JbaeTnO+rEqzCe1wtyQClv6q3YWq0voOj4E+bMZBErVS1GAHPDiRGONYq34M1/d8KuQMgvi6Gjw== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From f836abf4d3b2838ffd354a7e11fc889ad08e72c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 5 Aug 2022 03:33:41 +0200 Subject: [PATCH 20/92] migration of users and user_contacts including merge of email_optin content --- .../0045-adapt_users_table_for_gradidoid.ts | 162 +++++++++++++++--- 1 file changed, 136 insertions(+), 26 deletions(-) diff --git a/database/migrations/0045-adapt_users_table_for_gradidoid.ts b/database/migrations/0045-adapt_users_table_for_gradidoid.ts index 65c2d4b97..cc94ab7aa 100644 --- a/database/migrations/0045-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0045-adapt_users_table_for_gradidoid.ts @@ -7,20 +7,56 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { v4 as uuidv4 } from 'uuid'; + export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + /* await queryFn(` - CREATE FUNCTION UuidToBin(_uuid BINARY(36)) + CREATE FUNCTION uuid_v4s() + RETURNS CHAR(36) + BEGIN + -- 1th and 2nd block are made of 6 random bytes + SET @h1 = HEX(RANDOM_BYTES(4)); + SET @h2 = HEX(RANDOM_BYTES(2)); + + -- 3th block will start with a 4 indicating the version, remaining is random + SET @h3 = SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3); + + -- 4th block first nibble can only be 8, 9 A or B, remaining is random + SET @h4 = CONCAT(HEX(FLOOR(ASCII(RANDOM_BYTES(1)) / 64)+8), + SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3)); + + -- 5th block is made of 6 random bytes + SET @h5 = HEX(RANDOM_BYTES(6)); + + -- Build the complete UUID + RETURN LOWER(CONCAT( + @h1, '-', @h2, '-4', @h3, '-', @h4, '-', @h5 + )); + END`) + + + + SELECT LOWER(CONCAT( + HEX(RANDOM_BYTES(4)), '-', + HEX(RANDOM_BYTES(2)), '-4', + SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3), '-', + CONCAT(HEX(FLOOR(ASCII(RANDOM_BYTES(1)) / 64)+8),SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3)), '-', + HEX(RANDOM_BYTES(6)) + + + await queryFn( + `CREATE FUNCTION UuidToBin(_uuid BINARY(36)) RETURNS BINARY(16) LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER - RETURN + RETURN UNHEX(CONCAT( SUBSTR(_uuid, 15, 4), SUBSTR(_uuid, 10, 4), SUBSTR(_uuid, 1, 8), SUBSTR(_uuid, 20, 4), - SUBSTR(_uuid, 25) ));`) - - await queryFn(` + SUBSTR(_uuid, 25) )); + // CREATE FUNCTION UuidFromBin(_bin BINARY(16)) RETURNS BINARY(36) LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER @@ -31,15 +67,49 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis HEX(SUBSTR(_bin, 1, 2)), HEX(SUBSTR(_bin, 9, 2)), HEX(SUBSTR(_bin, 11)) - ));`) + )); + + // + DELIMITER ; + + + + CREATE FUNCTION BIN_TO_UUID(b BINARY(16)) + RETURNS CHAR(36) + BEGIN + DECLARE hexStr CHAR(32); + SET hexStr = HEX(b); + RETURN LOWER(CONCAT( + SUBSTR(hexStr, 1, 8), '-', + SUBSTR(hexStr, 9, 4), '-', + SUBSTR(hexStr, 13, 4), '-', + SUBSTR(hexStr, 17, 4), '-', + SUBSTR(hexStr, 21) + )); + END `) + + await queryFn(` DELIMITER ;`) + + await queryFn(`DELIMITER $$ + + CREATE FUNCTION UUID_TO_BIN(uuid CHAR(36)) + RETURNS BINARY(16) + BEGIN + RETURN UNHEX(REPLACE(uuid, '-', '')); + END + + $$ + + DELIMITER ;`) +*/ await queryFn(` CREATE TABLE IF NOT EXISTS \`user_contacts\` ( \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, \`type\` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, \`user_id\` int(10) unsigned NOT NULL, - \`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, - \`email_verification_code\` bigint(20) unsigned NOT NULL, + \`email\` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL UNIQUE, + \`email_verification_code\` bigint(20) unsigned NOT NULL UNIQUE, \`email_opt_in_type_id\` int NOT NULL, \`email_resend_count\` int DEFAULT '0', \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, @@ -51,33 +121,73 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis UNIQUE KEY \`email_verification_code\` (\`email_verification_code\`), UNIQUE KEY \`email\` (\`email\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) + console.log('user_contacts created...') + + // First add gradido_id as nullable column without Default + await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL UNIQUE AFTER `id`;') + console.log('users.gradido_id added...\n') + + // Second update gradido_id with ensured unique uuidv4 + console.log('search for all users with gradido_id is null...\n') + const usersToUpdate = await queryFn(`SELECT 'u.id', 'u.gradido_id' FROM 'users' as u WHERE 'u.gradido_id' is null`) + for (const id in usersToUpdate) { + const user = usersToUpdate[id] + console.log('found user: %s\n', user) + let gradidoId = null + let countIds = null + do { + gradidoId = uuidv4() + console.log('uuid: %s\n', gradidoId) + countIds = await queryFn('SELECT COUNT(*) FROM `users` as u WHERE u.gradido_id = ${gradidoId}') + console.log('found uuids: %d\n', countIds[0]) + } while (countIds[0] > 0) + await queryFn('UPDATE `users` SET `gradido_id` = ${gradidoId} WHERE `id` = ${user.id}') + console.log('update user with id=%d and gradidoId=%s\n', user.id, gradidoId) + } + + // third modify gradido_id to not nullable and unique + await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;') + console.log('alter users.gradido_id to NOT NULL and UNIQUE...\n') - await queryFn( - 'ALTER TABLE `users` ADD COLUMN `gradido_id` BINARY(16) NOT NULL UNIQUE DEFAULT UuidToBin(UUID()) AFTER `id`;', - ) await queryFn( 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', ) + console.log('users.alias added...\n') + await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') + console.log('users.email_id added...\n') + + // merge values from login_email_opt_in table with users.email in new user_contacts table await queryFn(` - INSERT INTO gradido_community.user_contacts - (type, user_id, email, email_verification_code, email_opt_in_type_id, email_resent_count, email_checked, created_at, updated_at, deleted_at) + INSERT INTO 'user_contacts' + ('type', 'user_id', 'email', 'email_verification_code', 'email_opt_in_type_id', 'email_resent_count', 'email_checked', 'created_at', 'updated_at', 'deleted_at') SELECT - 'EMAIL' as type, - u.id as user_id, - u.email, - e.verification_code, - e.email_opt_in_type_id, - e.resend_count, - u.email_checked, - e.created, - e.updated, - u.deletedAt + "EMAIL" as 'type', + 'u.id' as 'user_id', + 'u.email', + 'e.verification_code' as 'email_verification_code', + 'e.email_opt_in_type_id', + 'e.resend_count' as 'email_resent_count', + 'u.email_checked', + 'e.created as created_at', + 'e.updated as updated_at', + 'u.deletedAt as deleted_at' FROM - gradido_community.users as u, - gradido_community.login_email_opt_in as e + 'users' as u, + 'login_email_opt_in' as e WHERE - u.id = e.user_id;`) + 'u.id' = 'e.user_id';`) + console.log('user_contacts inserted...\n') + + // insert in users table the email_id of the new created email-contacts + const contacts = await queryFn(`SELECT 'c.id', 'c.user_id' FROM 'user_contacts' as c`) + for (const id in contacts) { + const contact = contacts[id] + console.log('found contact: %s\n', contact) + await queryFn(`UPDATE 'users' as u SET 'u.email_id' = ${contact.id} WHERE 'u.id' = ${contact.user_id}`) + console.log('update users with id=%d and email_id=%d\n', contact.user_id, contact.id) + } + console.log('upgrade finished...\n') } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { From dc76a4785d25f2bece24538b41fa206fe5db7a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 8 Aug 2022 18:33:56 +0200 Subject: [PATCH 21/92] complete installation of uuid package --- database/package.json | 4 +++- database/yarn.lock | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/database/package.json b/database/package.json index 23ab63f2b..4e3591bbb 100644 --- a/database/package.json +++ b/database/package.json @@ -37,6 +37,7 @@ "typescript": "^4.3.5" }, "dependencies": { + "@types/uuid": "^8.3.4", "cross-env": "^7.0.3", "crypto": "^1.0.1", "decimal.js-light": "^2.5.1", @@ -44,6 +45,7 @@ "mysql2": "^2.3.0", "reflect-metadata": "^0.1.13", "ts-mysql-migrate": "^1.0.2", - "typeorm": "^0.2.38" + "typeorm": "^0.2.38", + "uuid": "^8.3.2" } } diff --git a/database/yarn.lock b/database/yarn.lock index e5d74929c..b30db4595 100644 --- a/database/yarn.lock +++ b/database/yarn.lock @@ -137,6 +137,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5" integrity sha512-ho3Ruq+fFnBrZhUYI46n/bV2GjwzSkwuT4dTf0GkuNFmnb8nq4ny2z9JEVemFi6bdEJanHLlYfy9c6FN9B9McQ== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/zen-observable@0.8.3": version "0.8.3" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.3.tgz#781d360c282436494b32fe7d9f7f8e64b3118aa3" @@ -2088,6 +2093,11 @@ util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" From dea11232591f6e34ce25454fbd9a61e0eb81f08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 8 Aug 2022 18:34:38 +0200 Subject: [PATCH 22/92] first complete migration up and down run --- .../0045-adapt_users_table_for_gradidoid.ts | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/database/migrations/0045-adapt_users_table_for_gradidoid.ts b/database/migrations/0045-adapt_users_table_for_gradidoid.ts index cc94ab7aa..e33d5af1a 100644 --- a/database/migrations/0045-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0045-adapt_users_table_for_gradidoid.ts @@ -7,7 +7,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from 'uuid' export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { /* @@ -117,84 +117,88 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, \`updated_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, \`deleted_at\` datetime NULL DEFAULT NULL, - PRIMARY KEY (\`id\`), - UNIQUE KEY \`email_verification_code\` (\`email_verification_code\`), - UNIQUE KEY \`email\` (\`email\`) + PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - console.log('user_contacts created...') + // console.log('user_contacts created...') // First add gradido_id as nullable column without Default - await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL UNIQUE AFTER `id`;') - console.log('users.gradido_id added...\n') + await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL AFTER `id`;') + // console.log('users.gradido_id added...\n') // Second update gradido_id with ensured unique uuidv4 - console.log('search for all users with gradido_id is null...\n') - const usersToUpdate = await queryFn(`SELECT 'u.id', 'u.gradido_id' FROM 'users' as u WHERE 'u.gradido_id' is null`) + // console.log('search for all users with gradido_id is null...\n') + const usersToUpdate = await queryFn('SELECT `id`, `gradido_id` FROM `users`') // WHERE 'u.gradido_id' is null`,) for (const id in usersToUpdate) { const user = usersToUpdate[id] - console.log('found user: %s\n', user) + // console.log('found user: %s\n', user) let gradidoId = null let countIds = null do { gradidoId = uuidv4() - console.log('uuid: %s\n', gradidoId) - countIds = await queryFn('SELECT COUNT(*) FROM `users` as u WHERE u.gradido_id = ${gradidoId}') - console.log('found uuids: %d\n', countIds[0]) + // console.log('uuid: %s\n', gradidoId) + countIds = await queryFn( + `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, + ) + // console.log('found uuids: %d\n', countIds[0]) } while (countIds[0] > 0) - await queryFn('UPDATE `users` SET `gradido_id` = ${gradidoId} WHERE `id` = ${user.id}') - console.log('update user with id=%d and gradidoId=%s\n', user.id, gradidoId) + await queryFn( + `UPDATE \`users\` SET \`gradido_id\` = "${gradidoId}" WHERE \`id\` = "${user.id}"`, + ) + // console.log('update user with id=%d and gradidoId=%s\n', user.id, gradidoId) } // third modify gradido_id to not nullable and unique await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;') - console.log('alter users.gradido_id to NOT NULL and UNIQUE...\n') + // console.log('alter users.gradido_id to NOT NULL and UNIQUE...\n') await queryFn( 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', ) - console.log('users.alias added...\n') + // console.log('users.alias added...\n') await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') - console.log('users.email_id added...\n') - + // console.log('users.email_id added...\n') + // merge values from login_email_opt_in table with users.email in new user_contacts table await queryFn(` - INSERT INTO 'user_contacts' - ('type', 'user_id', 'email', 'email_verification_code', 'email_opt_in_type_id', 'email_resent_count', 'email_checked', 'created_at', 'updated_at', 'deleted_at') + INSERT INTO user_contacts + (type, user_id, email, email_verification_code, email_opt_in_type_id, email_resend_count, email_checked, created_at, updated_at, deleted_at) SELECT - "EMAIL" as 'type', - 'u.id' as 'user_id', - 'u.email', - 'e.verification_code' as 'email_verification_code', - 'e.email_opt_in_type_id', - 'e.resend_count' as 'email_resent_count', - 'u.email_checked', - 'e.created as created_at', - 'e.updated as updated_at', - 'u.deletedAt as deleted_at' + 'EMAIL', + u.id as user_id, + u.email, + e.verification_code as email_verification_code, + e.email_opt_in_type_id, + e.resend_count as email_resend_count, + u.email_checked, + e.created as created_at, + e.updated as updated_at, + u.deletedAt as deleted_at\ FROM - 'users' as u, - 'login_email_opt_in' as e + users as u, + login_email_opt_in as e WHERE - 'u.id' = 'e.user_id';`) - console.log('user_contacts inserted...\n') - + u.id = e.user_id;`) + // console.log('user_contacts inserted...\n') + // insert in users table the email_id of the new created email-contacts - const contacts = await queryFn(`SELECT 'c.id', 'c.user_id' FROM 'user_contacts' as c`) + const contacts = await queryFn(`SELECT c.id, c.user_id FROM user_contacts as c`) for (const id in contacts) { const contact = contacts[id] - console.log('found contact: %s\n', contact) - await queryFn(`UPDATE 'users' as u SET 'u.email_id' = ${contact.id} WHERE 'u.id' = ${contact.user_id}`) - console.log('update users with id=%d and email_id=%d\n', contact.user_id, contact.id) + // console.log('found contact: %s\n', contact) + await queryFn( + `UPDATE users as u SET u.email_id = "${contact.id}" WHERE u.id = "${contact.user_id}"`, + ) + // console.log('update users with id=%d and email_id=%d\n', contact.user_id, contact.id) } - console.log('upgrade finished...\n') + // console.log('upgrade finished...\n') } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { // write downgrade logic as parameter of queryFn - await queryFn(`DROP TABLE IF EXISTS \`user_contacts\`;`) + await queryFn(`DROP TABLE IF EXISTS user_contacts;`) - await queryFn('ALTER TABLE `users` DROP COLUMN `gradido_id`;') - await queryFn('ALTER TABLE `users` DROP COLUMN `alias`;') - await queryFn('ALTER TABLE `users` DROP COLUMN `email_id`;') + await queryFn('ALTER TABLE users DROP COLUMN gradido_id;') + await queryFn('ALTER TABLE users DROP COLUMN alias;') + await queryFn('ALTER TABLE users DROP COLUMN email_id;') } From f73f132d504f71d1caa6cb8703abad11b8f5ccff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 8 Aug 2022 19:24:07 +0200 Subject: [PATCH 23/92] remove email from user on dev_up and reconstruct on dev_down --- .../0045-adapt_users_table_for_gradidoid.ts | 120 ++---------------- 1 file changed, 13 insertions(+), 107 deletions(-) diff --git a/database/migrations/0045-adapt_users_table_for_gradidoid.ts b/database/migrations/0045-adapt_users_table_for_gradidoid.ts index e33d5af1a..8f3f83f28 100644 --- a/database/migrations/0045-adapt_users_table_for_gradidoid.ts +++ b/database/migrations/0045-adapt_users_table_for_gradidoid.ts @@ -10,99 +10,6 @@ import { v4 as uuidv4 } from 'uuid' export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { - /* - await queryFn(` - CREATE FUNCTION uuid_v4s() - RETURNS CHAR(36) - BEGIN - -- 1th and 2nd block are made of 6 random bytes - SET @h1 = HEX(RANDOM_BYTES(4)); - SET @h2 = HEX(RANDOM_BYTES(2)); - - -- 3th block will start with a 4 indicating the version, remaining is random - SET @h3 = SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3); - - -- 4th block first nibble can only be 8, 9 A or B, remaining is random - SET @h4 = CONCAT(HEX(FLOOR(ASCII(RANDOM_BYTES(1)) / 64)+8), - SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3)); - - -- 5th block is made of 6 random bytes - SET @h5 = HEX(RANDOM_BYTES(6)); - - -- Build the complete UUID - RETURN LOWER(CONCAT( - @h1, '-', @h2, '-4', @h3, '-', @h4, '-', @h5 - )); - END`) - - - - SELECT LOWER(CONCAT( - HEX(RANDOM_BYTES(4)), '-', - HEX(RANDOM_BYTES(2)), '-4', - SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3), '-', - CONCAT(HEX(FLOOR(ASCII(RANDOM_BYTES(1)) / 64)+8),SUBSTR(HEX(RANDOM_BYTES(2)), 2, 3)), '-', - HEX(RANDOM_BYTES(6)) - - - await queryFn( - `CREATE FUNCTION UuidToBin(_uuid BINARY(36)) - RETURNS BINARY(16) - LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER - RETURN - UNHEX(CONCAT( - SUBSTR(_uuid, 15, 4), - SUBSTR(_uuid, 10, 4), - SUBSTR(_uuid, 1, 8), - SUBSTR(_uuid, 20, 4), - SUBSTR(_uuid, 25) )); - // - CREATE FUNCTION UuidFromBin(_bin BINARY(16)) - RETURNS BINARY(36) - LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER - RETURN - LCASE(CONCAT_WS('-', - HEX(SUBSTR(_bin, 5, 4)), - HEX(SUBSTR(_bin, 3, 2)), - HEX(SUBSTR(_bin, 1, 2)), - HEX(SUBSTR(_bin, 9, 2)), - HEX(SUBSTR(_bin, 11)) - )); - - // - DELIMITER ; - - - - CREATE FUNCTION BIN_TO_UUID(b BINARY(16)) - RETURNS CHAR(36) - BEGIN - DECLARE hexStr CHAR(32); - SET hexStr = HEX(b); - RETURN LOWER(CONCAT( - SUBSTR(hexStr, 1, 8), '-', - SUBSTR(hexStr, 9, 4), '-', - SUBSTR(hexStr, 13, 4), '-', - SUBSTR(hexStr, 17, 4), '-', - SUBSTR(hexStr, 21) - )); - END `) - - await queryFn(` DELIMITER ;`) - - await queryFn(`DELIMITER $$ - - CREATE FUNCTION UUID_TO_BIN(uuid CHAR(36)) - RETURNS BINARY(16) - BEGIN - RETURN UNHEX(REPLACE(uuid, '-', '')); - END - - $$ - - DELIMITER ;`) -*/ - await queryFn(` CREATE TABLE IF NOT EXISTS \`user_contacts\` ( \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -119,45 +26,35 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`deleted_at\` datetime NULL DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) - // console.log('user_contacts created...') // First add gradido_id as nullable column without Default await queryFn('ALTER TABLE `users` ADD COLUMN `gradido_id` CHAR(36) NULL AFTER `id`;') - // console.log('users.gradido_id added...\n') // Second update gradido_id with ensured unique uuidv4 - // console.log('search for all users with gradido_id is null...\n') const usersToUpdate = await queryFn('SELECT `id`, `gradido_id` FROM `users`') // WHERE 'u.gradido_id' is null`,) for (const id in usersToUpdate) { const user = usersToUpdate[id] - // console.log('found user: %s\n', user) let gradidoId = null let countIds = null do { gradidoId = uuidv4() - // console.log('uuid: %s\n', gradidoId) countIds = await queryFn( `SELECT COUNT(*) FROM \`users\` WHERE \`gradido_id\` = "${gradidoId}"`, ) - // console.log('found uuids: %d\n', countIds[0]) } while (countIds[0] > 0) await queryFn( `UPDATE \`users\` SET \`gradido_id\` = "${gradidoId}" WHERE \`id\` = "${user.id}"`, ) - // console.log('update user with id=%d and gradidoId=%s\n', user.id, gradidoId) } // third modify gradido_id to not nullable and unique await queryFn('ALTER TABLE `users` MODIFY COLUMN `gradido_id` CHAR(36) NOT NULL UNIQUE;') - // console.log('alter users.gradido_id to NOT NULL and UNIQUE...\n') await queryFn( 'ALTER TABLE `users` ADD COLUMN `alias` varchar(20) NULL UNIQUE AFTER `gradido_id`;', ) - // console.log('users.alias added...\n') await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') - // console.log('users.email_id added...\n') // merge values from login_email_opt_in table with users.email in new user_contacts table await queryFn(` @@ -179,22 +76,31 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis login_email_opt_in as e WHERE u.id = e.user_id;`) - // console.log('user_contacts inserted...\n') // insert in users table the email_id of the new created email-contacts const contacts = await queryFn(`SELECT c.id, c.user_id FROM user_contacts as c`) for (const id in contacts) { const contact = contacts[id] - // console.log('found contact: %s\n', contact) await queryFn( `UPDATE users as u SET u.email_id = "${contact.id}" WHERE u.id = "${contact.user_id}"`, ) - // console.log('update users with id=%d and email_id=%d\n', contact.user_id, contact.id) } - // console.log('upgrade finished...\n') + // this step comes after verification and test + await queryFn('ALTER TABLE users DROP COLUMN email;') } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + // reconstruct the previous email back from contacts to users table + await queryFn('ALTER TABLE users ADD COLUMN email varchar(255) NULL AFTER privkey;') + const contacts = await queryFn(`SELECT c.id, c.email, c.user_id FROM user_contacts as c`) + for (const id in contacts) { + const contact = contacts[id] + await queryFn( + `UPDATE users SET email = "${contact.email}" WHERE id = "${contact.user_id}" and email_id = "${contact.id}"`, + ) + } + await queryFn('ALTER TABLE users MODIFY COLUMN email varchar(255) NOT NULL UNIQUE;') + // write downgrade logic as parameter of queryFn await queryFn(`DROP TABLE IF EXISTS user_contacts;`) From fadbc7068e9f38fdce056ff9445d2407ab43da55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 9 Aug 2022 03:56:22 +0200 Subject: [PATCH 24/92] start adaptions of users changes in backend --- backend/package.json | 1 + backend/src/graphql/enum/UserContactType.ts | 11 ++ backend/src/graphql/model/User.ts | 4 +- backend/src/graphql/resolver/UserResolver.ts | 143 ++++++++++++++++-- backend/src/util/communityUser.ts | 5 +- backend/yarn.lock | 5 + .../User.ts | 16 +- .../UserContact.ts | 13 +- 8 files changed, 176 insertions(+), 22 deletions(-) create mode 100644 backend/src/graphql/enum/UserContactType.ts diff --git a/backend/package.json b/backend/package.json index 50f26351d..bb4ab3e51 100644 --- a/backend/package.json +++ b/backend/package.json @@ -19,6 +19,7 @@ "dependencies": { "@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", diff --git a/backend/src/graphql/enum/UserContactType.ts b/backend/src/graphql/enum/UserContactType.ts new file mode 100644 index 000000000..93c83830c --- /dev/null +++ b/backend/src/graphql/enum/UserContactType.ts @@ -0,0 +1,11 @@ +import { registerEnumType } from 'type-graphql' + +export enum UserContactType { + USER_CONTACT_EMAIL = 'EMAIL', + USER_CONTACT_PHONE = 'PHONE', +} + +registerEnumType(UserContactType, { + name: 'UserContactType', // this one is mandatory + description: 'Type of the user contact', // this one is optional +}) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 0642be630..3ea4e2c05 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -8,12 +8,12 @@ import { FULL_CREATION_AVAILABLE } from '../resolver/const/const' export class User { constructor(user: dbUser, creation: Decimal[] = FULL_CREATION_AVAILABLE) { this.id = user.id - this.email = user.email + this.email = user.emailContact.email this.firstName = user.firstName this.lastName = user.lastName this.deletedAt = user.deletedAt this.createdAt = user.createdAt - this.emailChecked = user.emailChecked + this.emailChecked = user.emailContact.emailChecked this.language = user.language this.publisherId = user.publisherId this.isAdmin = user.isAdmin diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index f61414e42..687cad68b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -1,12 +1,12 @@ import fs from 'fs' import { backendLogger as logger } from '@/server/logger' - import { Context, getUser } from '@/server/context' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' import { getConnection } from '@dbTools/typeorm' import CONFIG from '@/config' import { User } from '@model/User' import { User as DbUser } from '@entity/User' +import { UserContact as DbUserContact } from '@entity/UserContact' import { communityDbUser } from '@/util/communityUser' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { ContributionLink as dbContributionLink } from '@entity/ContributionLink' @@ -32,6 +32,7 @@ import { EventSendConfirmationEmail, } from '@/event/Event' import { getUserCreation } from './util/creations' +import { UserContactType } from '../enum/UserContactType' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -172,6 +173,19 @@ const SecretKeyCryptographyDecrypt = (encryptedMessage: Buffer, encryptionKey: B return message } +const newEmailContact = (email: string, userId: number): DbUserContact => { + logger.trace(`newEmailContact...`) + const emailContact = new DbUserContact() + emailContact.email = email + emailContact.userId = userId + emailContact.type = UserContactType.USER_CONTACT_EMAIL + emailContact.emailChecked = false + emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER + emailContact.emailVerificationCode = random(64) + logger.debug(`newEmailContact...successful: ${emailContact}`) + return emailContact +} + const newEmailOptIn = (userId: number): LoginEmailOptIn => { logger.trace('newEmailOptIn...') const emailOptIn = new LoginEmailOptIn() @@ -182,6 +196,7 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => { return emailOptIn } +/* // needed by AdminResolver // checks if given code exists and can be resent // if optIn does not exits, it is created @@ -218,6 +233,36 @@ export const checkOptInCode = async ( logger.debug(`checkOptInCode...successful: ${optInCode} for userid=${userId}`) return optInCode } +*/ +export const checkEmailVerificationCode = async ( + emailContact: DbUserContact, + optInType: OptInType = OptInType.EMAIL_OPT_IN_REGISTER +): Promise => { + logger.info(`checkEmailVerificationCode... ${emailContact}`) + if (emailContact.updatedAt) { + if (!canEmailResend(emailContact.updatedAt)) { + logger.error(`email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`) + throw new Error( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ) + } + emailContact.updatedAt = new Date() + emailContact.emailResendCount++ + } else { + logger.trace('create new EmailVerificationCode for userId=' + emailContact.userId) + emailContact.emailChecked = false + emailContact.emailVerificationCode = random(64) + } + emailContact.emailOptInTypeId = optInType + await DbUserContact.save(emailContact).catch(() => { + logger.error('Unable to save email verification code= ' + emailContact) + throw new Error('Unable to save email verification code.') + }) + logger.debug(`checkEmailVerificationCode...successful: ${emailContact}`) + return emailContact +} export const activationLink = (optInCode: LoginEmailOptIn): string => { logger.debug(`activationLink(${LoginEmailOptIn})...`) @@ -251,15 +296,31 @@ export class UserResolver { ): Promise { logger.info(`login with ${email}, ***, ${publisherId} ...`) email = email.trim().toLowerCase() + const dbUser = await findUserByEmail(email) + /* + const dbUserContact = await DbUserContact.findOneOrFail({ email }, { withDeleted: true }).catch( + () => { + logger.error(`UserContact with email=${email} does not exists`) + throw new Error('No user with this credentials') + }, + ) + const userId = dbUserContact.userId + const dbUser = await DbUser.findOneOrFail(userId).catch(() => { + logger.error(`User with emeilContact=${email} connected per userId=${userId} does not exist`) + throw new Error('No user with this credentials') + }) + */ + /* const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => { logger.error(`User with email=${email} does not exists`) throw new Error('No user with this credentials') }) + */ if (dbUser.deletedAt) { logger.error('The User was permanently deleted in database.') throw new Error('This user was permanently deleted. Contact support for questions.') } - if (!dbUser.emailChecked) { + if (!dbUser.emailContact.emailChecked) { logger.error('The Users email is not validate yet.') throw new Error('User email not validated') } @@ -339,11 +400,13 @@ export class UserResolver { // Validate email unique email = email.trim().toLowerCase() - // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes - const userFound = await DbUser.findOne({ email }, { withDeleted: true }) - logger.info(`DbUser.findOne(email=${email}) = ${userFound}`) + const foundUser = await findUserByEmail(email) - if (userFound) { + // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes + // const userFound = await DbUser.findOne({ email }, { withDeleted: true }) + logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) + + if (foundUser) { logger.info('User already exists with this email=' + email) // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. @@ -382,8 +445,11 @@ export class UserResolver { const eventRegister = new EventRegister() const eventRedeemRegister = new EventRedeemRegister() const eventSendConfirmEmail = new EventSendConfirmationEmail() + // const dbEmailContact = new DbUserContact() + // dbEmailContact.email = email + const dbUser = new DbUser() - dbUser.email = email + // dbUser.emailContact = dbEmailContact dbUser.firstName = firstName dbUser.lastName = lastName // dbUser.emailHash = emailHash @@ -426,16 +492,29 @@ export class UserResolver { logger.error('Error while saving dbUser', error) throw new Error('error saving user') }) + const emailContact = newEmailContact(email, dbUser.id) + await queryRunner.manager.save(emailContact).catch((error) => { + logger.error('Error while saving emailContact', error) + throw new Error('error saving email user contact') + }) + dbUser.emailContact = emailContact + await queryRunner.manager.save(dbUser).catch((error) => { + logger.error('Error while updating dbUser', error) + throw new Error('error updating user') + }) + + /* const emailOptIn = newEmailOptIn(dbUser.id) await queryRunner.manager.save(emailOptIn).catch((error) => { logger.error('Error while saving emailOptIn', error) throw new Error('error saving email opt in') }) + */ const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( /{optin}/g, - emailOptIn.verificationCode.toString(), + emailContact.emailVerificationCode.toString(), ).replace(/{code}/g, redeemCode ? '/' + redeemCode : '') // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -482,16 +561,19 @@ export class UserResolver { async forgotPassword(@Arg('email') email: string): Promise { logger.info(`forgotPassword(${email})...`) email = email.trim().toLowerCase() - const user = await DbUser.findOne({ email }) + const user = await findUserByEmail(email) + // const user = await DbUser.findOne({ email }) if (!user) { logger.warn(`no user found with ${email}`) return true } // can be both types: REGISTER and RESET_PASSWORD - let optInCode = await LoginEmailOptIn.findOne({ - userId: user.id, - }) + // let optInCode = await LoginEmailOptIn.findOne({ + // userId: user.id, + // }) + let optInCode = user.emailContact.emailVerificationCode + optInCode = await checkEmailVerificationCode(user.emailContact, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) optInCode = await checkOptInCode(optInCode, user.id, OptInType.EMAIL_OPT_IN_RESET_PASSWORD) logger.info(`optInCode for ${email}=${optInCode}`) @@ -727,25 +809,55 @@ export class UserResolver { logger.info('missing context.user for EloPage-check') return false } - const elopageBuys = hasElopageBuys(userEntity.email) + const elopageBuys = hasElopageBuys(userEntity.emailContact.email) logger.debug(`has ElopageBuys = ${elopageBuys}`) return elopageBuys } } +async function findUserByEmail(email: string): Promise { + const dbUserContact = await DbUserContact.findOneOrFail(email, { withDeleted: true }).catch( + () => { + logger.error(`UserContact with email=${email} does not exists`) + throw new Error('No user with this credentials') + }, + ) + const userId = dbUserContact.userId + const dbUser = await DbUser.findOneOrFail(userId).catch(() => { + logger.error(`User with emeilContact=${email} connected per userId=${userId} does not exist`) + throw new Error('No user with this credentials') + }) + return dbUser +} + +/* const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => { const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime() // time is given in minutes return timeElapsed <= duration * 60 * 1000 } - +*/ +const isTimeExpired = (updatedAt: Date, duration: number): boolean => { + const timeElapsed = Date.now() - new Date(updatedAt).getTime() + // time is given in minutes + return timeElapsed <= duration * 60 * 1000 +} +/* const isOptInValid = (optIn: LoginEmailOptIn): boolean => { return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME) } - +*/ +const isEmailVerificationCodeValid = (updatedAt: Date): boolean => { + return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME) +} +/* const canResendOptIn = (optIn: LoginEmailOptIn): boolean => { return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME) } +*/ +const canEmailResend = (updatedAt: Date): boolean => { + return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME) +} const getTimeDurationObject = (time: number): { hours?: number; minutes: number } => { if (time > 60) { @@ -763,3 +875,4 @@ export const printTimeDuration = (duration: number): string => { if (time.hours) return `${time.hours} hours` + (result !== '' ? ` and ${result}` : '') return result } + diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index c90e786c6..65dee6728 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -2,13 +2,14 @@ import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' import { User as dbUser } from '@entity/User' +// import { UserContact as EmailContact } from '@entity/UserContact' import { User } from '@model/User' const communityDbUser: dbUser = { id: -1, gradidoID: '11111111-2222-3333-4444-55555555', alias: '', - email: 'support@gradido.net', + // email: 'support@gradido.net', firstName: 'Gradido', lastName: 'Akademie', pubKey: Buffer.from(''), @@ -17,7 +18,7 @@ const communityDbUser: dbUser = { password: BigInt(0), // emailHash: Buffer.from(''), createdAt: new Date(), - emailChecked: false, + // emailChecked: false, language: '', isAdmin: null, publisherId: 0, diff --git a/backend/yarn.lock b/backend/yarn.lock index 53a53cb9b..731404d6d 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1000,6 +1000,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@^8.3.4": + version "8.3.4" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + "@types/validator@^13.1.3": version "13.6.3" resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.6.3.tgz#31ca2e997bf13a0fffca30a25747d5b9f7dbb7de" diff --git a/database/entity/0045-adapt_users_table_for_gradidoid/User.ts b/database/entity/0045-adapt_users_table_for_gradidoid/User.ts index 1e7e9d8d8..69a085a87 100644 --- a/database/entity/0045-adapt_users_table_for_gradidoid/User.ts +++ b/database/entity/0045-adapt_users_table_for_gradidoid/User.ts @@ -6,8 +6,10 @@ import { DeleteDateColumn, OneToMany, JoinColumn, + OneToOne, } from 'typeorm' import { Contribution } from '../Contribution' +import { UserContact } from '../UserContact' @Entity('users', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class User extends BaseEntity { @@ -37,11 +39,18 @@ export class User extends BaseEntity { @Column({ name: 'privkey', type: 'binary', length: 80, default: null, nullable: true }) privKey: Buffer + /* @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) email: string + */ + @OneToOne(() => UserContact, { primary: true, cascade: true }) + @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', @@ -69,9 +78,10 @@ export class User extends BaseEntity { @Column({ name: 'created', default: () => 'CURRENT_TIMESTAMP', nullable: false }) createdAt: Date - + /* @Column({ name: 'email_checked', type: 'bool', nullable: false, default: false }) emailChecked: boolean + */ @Column({ length: 4, default: 'de', collation: 'utf8mb4_unicode_ci', nullable: false }) language: string @@ -106,4 +116,8 @@ export class User extends BaseEntity { @OneToMany(() => Contribution, (contribution) => contribution.user) @JoinColumn({ name: 'user_id' }) contributions?: Contribution[] + + @OneToMany(() => UserContact, (usercontact) => usercontact.userId) + @JoinColumn({ name: 'user_id' }) + usercontacts?: UserContact[] } diff --git a/database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts b/database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts index fee0afeda..7c2dff3db 100644 --- a/database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts +++ b/database/entity/0045-adapt_users_table_for_gradidoid/UserContact.ts @@ -20,8 +20,17 @@ export class UserContact extends BaseEntity { @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) email: string - @Column({ name: 'email_hash', type: 'binary', length: 32, default: null, nullable: true }) - emailHash: Buffer + @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 From 14bd9aae81f466871fa8153d7eea18344f3df4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 18 Aug 2022 19:09:20 +0200 Subject: [PATCH 25/92] linting --- backend/src/util/communityUser.ts | 98 +++++++++++++++---------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 45fb6d4fb..2f0743270 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -1,49 +1,49 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - -import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' -import { User as dbUser } from '@entity/User' -import { UserContact } from '@entity/UserContact' -// import { UserContact as EmailContact } from '@entity/UserContact' -import { User } from '@model/User' - -const communityDbUser: dbUser = { - id: -1, - gradidoID: '11111111-2222-4333-4444-55555555', - alias: '', - // email: 'support@gradido.net', - emailContact: new UserContact(), - firstName: 'Gradido', - lastName: 'Akademie', - pubKey: Buffer.from(''), - privKey: Buffer.from(''), - deletedAt: null, - password: BigInt(0), - // emailHash: Buffer.from(''), - createdAt: new Date(), - // emailChecked: false, - language: '', - isAdmin: null, - publisherId: 0, - passphrase: '', - hasId: function (): boolean { - throw new Error('Function not implemented.') - }, - save: function (options?: SaveOptions): Promise { - throw new Error('Function not implemented.') - }, - remove: function (options?: RemoveOptions): Promise { - throw new Error('Function not implemented.') - }, - softRemove: function (options?: SaveOptions): Promise { - throw new Error('Function not implemented.') - }, - recover: function (options?: SaveOptions): Promise { - throw new Error('Function not implemented.') - }, - reload: function (): Promise { - throw new Error('Function not implemented.') - }, -} -const communityUser = new User(communityDbUser) - -export { communityDbUser, communityUser } +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { SaveOptions, RemoveOptions } from '@dbTools/typeorm' +import { User as dbUser } from '@entity/User' +import { UserContact } from '@entity/UserContact' +// import { UserContact as EmailContact } from '@entity/UserContact' +import { User } from '@model/User' + +const communityDbUser: dbUser = { + id: -1, + gradidoID: '11111111-2222-4333-4444-55555555', + alias: '', + // email: 'support@gradido.net', + emailContact: new UserContact(), + firstName: 'Gradido', + lastName: 'Akademie', + pubKey: Buffer.from(''), + privKey: Buffer.from(''), + deletedAt: null, + password: BigInt(0), + // emailHash: Buffer.from(''), + createdAt: new Date(), + // emailChecked: false, + language: '', + isAdmin: null, + publisherId: 0, + passphrase: '', + hasId: function (): boolean { + throw new Error('Function not implemented.') + }, + save: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + remove: function (options?: RemoveOptions): Promise { + throw new Error('Function not implemented.') + }, + softRemove: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + recover: function (options?: SaveOptions): Promise { + throw new Error('Function not implemented.') + }, + reload: function (): Promise { + throw new Error('Function not implemented.') + }, +} +const communityUser = new User(communityDbUser) + +export { communityDbUser, communityUser } From fde236aa43104616635db94b2e22d2aca26e1b6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 25 Aug 2022 01:06:25 +0200 Subject: [PATCH 26/92] add migration of users, user_contacts and email_opt_in --- backend/src/config/index.ts | 2 +- .../User.ts | 0 .../UserContact.ts | 0 database/entity/User.ts | 2 +- database/entity/UserContact.ts | 2 +- ...user_contacts_table.ts => 0048-add_user_contacts_table.ts} | 4 +++- 6 files changed, 6 insertions(+), 4 deletions(-) rename database/entity/{0047-add_user_contacts_table => 0048-add_user_contacts_table}/User.ts (100%) rename database/entity/{0047-add_user_contacts_table => 0048-add_user_contacts_table}/UserContact.ts (100%) rename database/migrations/{0047-add_user_contacts_table.ts => 0048-add_user_contacts_table.ts} (98%) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index ae73fa8ac..62b09c93d 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0047-messages_tables', + DB_VERSION: '0048-add_user_contacts_table', DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info diff --git a/database/entity/0047-add_user_contacts_table/User.ts b/database/entity/0048-add_user_contacts_table/User.ts similarity index 100% rename from database/entity/0047-add_user_contacts_table/User.ts rename to database/entity/0048-add_user_contacts_table/User.ts diff --git a/database/entity/0047-add_user_contacts_table/UserContact.ts b/database/entity/0048-add_user_contacts_table/UserContact.ts similarity index 100% rename from database/entity/0047-add_user_contacts_table/UserContact.ts rename to database/entity/0048-add_user_contacts_table/UserContact.ts diff --git a/database/entity/User.ts b/database/entity/User.ts index 1e0017b72..3191148ee 100644 --- a/database/entity/User.ts +++ b/database/entity/User.ts @@ -1 +1 @@ -export { User } from './0047-add_user_contacts_table/User' +export { User } from './0048-add_user_contacts_table/User' diff --git a/database/entity/UserContact.ts b/database/entity/UserContact.ts index e596489da..dfa1ab4a1 100644 --- a/database/entity/UserContact.ts +++ b/database/entity/UserContact.ts @@ -1 +1 @@ -export { UserContact } from './0047-add_user_contacts_table/UserContact' +export { UserContact } from './0048-add_user_contacts_table/UserContact' diff --git a/database/migrations/0047-add_user_contacts_table.ts b/database/migrations/0048-add_user_contacts_table.ts similarity index 98% rename from database/migrations/0047-add_user_contacts_table.ts rename to database/migrations/0048-add_user_contacts_table.ts index b3c6be03e..49f647e39 100644 --- a/database/migrations/0047-add_user_contacts_table.ts +++ b/database/migrations/0048-add_user_contacts_table.ts @@ -63,8 +63,10 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { - // reconstruct the previous email back from contacts to users table + // this step comes after verification and test await queryFn('ALTER TABLE users ADD COLUMN email varchar(255) NULL AFTER privkey;') + + // reconstruct the previous email back from contacts to users table const contacts = await queryFn(`SELECT c.id, c.email, c.user_id FROM user_contacts as c`) for (const id in contacts) { const contact = contacts[id] From 228451574daa6d1d67c854a4f8b7a6039e2e3b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 26 Aug 2022 02:28:04 +0200 Subject: [PATCH 27/92] adapt backend on database migration of UserContacts --- backend/src/event/Event.ts | 10 ++ backend/src/event/EventProtocolType.ts | 1 + .../graphql/model/UnconfirmedContribution.ts | 2 +- backend/src/graphql/model/User.ts | 12 +- backend/src/graphql/model/UserAdmin.ts | 4 +- backend/src/graphql/model/UserContact.ts | 56 +++++++++ backend/src/graphql/resolver/AdminResolver.ts | 83 +++++++++---- backend/src/graphql/resolver/GdtResolver.ts | 4 +- .../graphql/resolver/TransactionResolver.ts | 21 ++-- backend/src/graphql/resolver/UserResolver.ts | 115 ++++++++++-------- backend/src/typeorm/repository/User.ts | 8 +- backend/src/util/communityUser.ts | 1 + backend/src/webhook/elopage.ts | 4 +- .../0048-add_user_contacts_table/User.ts | 4 +- .../UserContact.ts | 15 ++- .../0048-add_user_contacts_table.ts | 2 +- 16 files changed, 245 insertions(+), 97 deletions(-) create mode 100644 backend/src/graphql/model/UserContact.ts diff --git a/backend/src/event/Event.ts b/backend/src/event/Event.ts index 6f07661f1..85fba896d 100644 --- a/backend/src/event/Event.ts +++ b/backend/src/event/Event.ts @@ -32,6 +32,7 @@ export class EventRegister extends EventBasicUserId {} export class EventRedeemRegister extends EventBasicRedeem {} export class EventInactiveAccount extends EventBasicUserId {} export class EventSendConfirmationEmail extends EventBasicUserId {} +export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {} export class EventConfirmationEmail extends EventBasicUserId {} export class EventRegisterEmailKlicktipp extends EventBasicUserId {} export class EventLogin extends EventBasicUserId {} @@ -113,6 +114,15 @@ export class Event { return this } + public setEventSendAccountMultiRegistrationEmail( + ev: EventSendAccountMultiRegistrationEmail, + ): Event { + this.setByBasicUser(ev.userId) + this.type = EventProtocolType.SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL + + return this + } + public setEventConfirmationEmail(ev: EventConfirmationEmail): Event { this.setByBasicUser(ev.userId) this.type = EventProtocolType.CONFIRM_EMAIL diff --git a/backend/src/event/EventProtocolType.ts b/backend/src/event/EventProtocolType.ts index 0f61f787a..52bcf8349 100644 --- a/backend/src/event/EventProtocolType.ts +++ b/backend/src/event/EventProtocolType.ts @@ -5,6 +5,7 @@ export enum EventProtocolType { REDEEM_REGISTER = 'REDEEM_REGISTER', INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL', + SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTI_REGISTRATION_EMAIL', CONFIRM_EMAIL = 'CONFIRM_EMAIL', REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP', LOGIN = 'LOGIN', diff --git a/backend/src/graphql/model/UnconfirmedContribution.ts b/backend/src/graphql/model/UnconfirmedContribution.ts index 1d697a971..a81bb4a49 100644 --- a/backend/src/graphql/model/UnconfirmedContribution.ts +++ b/backend/src/graphql/model/UnconfirmedContribution.ts @@ -13,7 +13,7 @@ export class UnconfirmedContribution { this.date = contribution.contributionDate this.firstName = user ? user.firstName : '' this.lastName = user ? user.lastName : '' - this.email = user ? user.email : '' + this.email = user ? user.emailContact.email : '' this.creation = creations } diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 728851ec2..a28fe4b69 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -3,6 +3,7 @@ import { KlickTipp } from './KlickTipp' import { User as dbUser } from '@entity/User' import Decimal from 'decimal.js-light' import { FULL_CREATION_AVAILABLE } from '../resolver/const/const' +import { UserContact } from './UserContact' @ObjectType() export class User { @@ -10,8 +11,9 @@ export class User { this.id = user.id this.gradidoID = user.gradidoID this.alias = user.alias - // this.email = user.email + this.emailId = user.emailId this.email = user.emailContact.email + this.emailContact = user.emailContact this.firstName = user.firstName this.lastName = user.lastName this.deletedAt = user.deletedAt @@ -35,12 +37,18 @@ export class User { gradidoID: string @Field(() => String, { nullable: true }) - alias: string + alias?: string + + @Field(() => Number, { nullable: true }) + emailId: number | null // TODO privacy issue here @Field(() => String) email: string + @Field(() => UserContact) + emailContact: UserContact + @Field(() => String, { nullable: true }) firstName: string | null diff --git a/backend/src/graphql/model/UserAdmin.ts b/backend/src/graphql/model/UserAdmin.ts index cf3663e70..08dc405ac 100644 --- a/backend/src/graphql/model/UserAdmin.ts +++ b/backend/src/graphql/model/UserAdmin.ts @@ -6,11 +6,11 @@ import { User } from '@entity/User' export class UserAdmin { constructor(user: User, creation: Decimal[], hasElopage: boolean, emailConfirmationSend: string) { this.userId = user.id - this.email = user.email + this.email = user.emailContact.email this.firstName = user.firstName this.lastName = user.lastName this.creation = creation - this.emailChecked = user.emailChecked + this.emailChecked = user.emailContact.emailChecked this.hasElopage = hasElopage this.deletedAt = user.deletedAt this.emailConfirmationSend = emailConfirmationSend diff --git a/backend/src/graphql/model/UserContact.ts b/backend/src/graphql/model/UserContact.ts new file mode 100644 index 000000000..902e2f9f2 --- /dev/null +++ b/backend/src/graphql/model/UserContact.ts @@ -0,0 +1,56 @@ +import { ObjectType, Field } from 'type-graphql' +import { UserContact as dbUserCOntact} from '@entity/UserContact' + +@ObjectType() +export class UserContact { + constructor(userContact: dbUserCOntact) { + this.id = userContact.id + this.type = userContact.type + this.userId = userContact.userId + this.email = userContact.email + // this.emailVerificationCode = userContact.emailVerificationCode + this.emailOptInTypeId = userContact.emailOptInTypeId + this.emailResendCount = userContact.emailResendCount + this.emailChecked = userContact.emailChecked + this.phone = userContact.phone + this.createdAt = userContact.createdAt + this.updatedAt = userContact.updatedAt + this.deletedAt = userContact.deletedAt + } + + @Field(() => Number) + id: number + + @Field(() => String) + type: string + + @Field(() => Number) + userId: number + + @Field(() => String) + email: string + + // @Field(() => BigInt, { nullable: true }) + // emailVerificationCode: BigInt | null + + @Field(() => Number, { nullable: true }) + emailOptInTypeId: number | null + + @Field(() => Number, { nullable: true }) + emailResendCount: number | null + + @Field(() => Boolean) + emailChecked: boolean + + @Field(() => String, { nullable: true }) + phone: string | null + + @Field(() => Date) + createdAt: Date + + @Field(() => Date, { nullable: true }) + updatedAt: Date | null + + @Field(() => Date, { nullable: true }) + deletedAt: Date | null +} diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index e70fe71ee..5d283026d 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -44,7 +44,7 @@ import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' -import { checkOptInCode, activationLink, printTimeDuration } from './UserResolver' +import { checkEmailVerificationCode, activationLink, printTimeDuration } from './UserResolver' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -62,6 +62,7 @@ import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, } from './const/const' +import { UserContact } from '@entity/UserContact' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -118,7 +119,8 @@ export class AdminResolver { const adminUsers = await Promise.all( users.map(async (user) => { let emailConfirmationSend = '' - if (!user.emailChecked) { + if (!user.emailContact.emailChecked) { + /* const emailOptIn = await LoginEmailOptIn.findOne( { userId: user.id, @@ -138,12 +140,18 @@ export class AdminResolver { emailConfirmationSend = emailOptIn.createdAt.toISOString() } } + */ + if (user.emailContact.updatedAt) { + emailConfirmationSend = user.emailContact.updatedAt.toISOString() + } else { + emailConfirmationSend = user.emailContact.createdAt.toISOString() + } } const userCreations = creations.find((c) => c.id === user.id) const adminUser = new UserAdmin( user, userCreations ? userCreations.creations : FULL_CREATION_AVAILABLE, - await hasElopageBuys(user.email), + await hasElopageBuys(user.emailContact.email), emailConfirmationSend, ) return adminUser @@ -239,24 +247,27 @@ export class AdminResolver { @Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs, @Ctx() context: Context, ): Promise { - const user = await dbUser.findOne({ email }, { withDeleted: true }) - if (!user) { + const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) + if (!emailContact) { + logger.error(`Could not find user with email: ${email}`) throw new Error(`Could not find user with email: ${email}`) } - if (user.deletedAt) { - throw new Error('This user was deleted. Cannot create a contribution.') + if (emailContact.deletedAt) { + logger.error('This emailContact was deleted. Cannot create a contribution.') + throw new Error('This emailContact was deleted. Cannot create a contribution.') } - if (!user.emailChecked) { + if (!emailContact.emailChecked) { + logger.error('Contribution could not be saved, Email is not activated') throw new Error('Contribution could not be saved, Email is not activated') } const moderator = getUser(context) logger.trace('moderator: ', moderator.id) - const creations = await getUserCreation(user.id) + const creations = await getUserCreation(emailContact.userId) logger.trace('creations', creations) const creationDateObj = new Date(creationDate) validateContribution(creations, amount, creationDateObj) const contribution = Contribution.create() - contribution.userId = user.id + contribution.userId = emailContact.userId contribution.amount = amount contribution.createdAt = new Date() contribution.contributionDate = creationDateObj @@ -267,7 +278,7 @@ export class AdminResolver { logger.trace('contribution to save', contribution) await Contribution.save(contribution) - return getUserCreation(user.id) + return getUserCreation(emailContact.userId) } @Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTIONS]) @@ -303,11 +314,18 @@ export class AdminResolver { @Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs, @Ctx() context: Context, ): Promise { - const user = await dbUser.findOne({ email }, { withDeleted: true }) + const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) + if (!emailContact) { + logger.error(`Could not find UserContact with email: ${email}`) + throw new Error(`Could not find UserContact with email: ${email}`) + } + const user = await dbUser.findOne({ id: emailContact.userId }, { withDeleted: true }) if (!user) { - throw new Error(`Could not find user with email: ${email}`) + logger.error(`Could not find User to emailContact: ${email}`) + throw new Error(`Could not find User to emailContact: ${email}`) } if (user.deletedAt) { + logger.error(`User was deleted (${email})`) throw new Error(`User was deleted (${email})`) } @@ -318,14 +336,17 @@ export class AdminResolver { }) if (!contributionToUpdate) { + logger.error('No contribution found to given id.') throw new Error('No contribution found to given id.') } if (contributionToUpdate.userId !== user.id) { + logger.error('user of the pending contribution and send user does not correspond') throw new Error('user of the pending contribution and send user does not correspond') } if (contributionToUpdate.moderatorId === null) { + logger.error('An admin is not allowed to update a user contribution.') throw new Error('An admin is not allowed to update a user contribution.') } @@ -379,7 +400,7 @@ export class AdminResolver { moderator: contribution.moderatorId, firstName: user ? user.firstName : '', lastName: user ? user.lastName : '', - email: user ? user.email : '', + email: user ? user.emailContact.email : '', creation: creation ? creation.creations : FULL_CREATION_AVAILABLE, } }) @@ -390,10 +411,10 @@ export class AdminResolver { async adminDeleteContribution(@Arg('id', () => Int) id: number): Promise { const contribution = await Contribution.findOne(id) if (!contribution) { + logger.error(`Contribution not found for given id: ${id}`) throw new Error('Contribution not found for given id.') } contribution.contributionStatus = ContributionStatus.DELETED - await contribution.save() const res = await contribution.softRemove() return !!res } @@ -406,15 +427,19 @@ export class AdminResolver { ): Promise { const contribution = await Contribution.findOne(id) if (!contribution) { + logger.error(`Contribution not found for given id: ${id}`) throw new Error('Contribution not found to given id.') } const moderatorUser = getUser(context) - if (moderatorUser.id === contribution.userId) + if (moderatorUser.id === contribution.userId) { + logger.error('Moderator can not confirm own contribution') throw new Error('Moderator can not confirm own contribution') - + } const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true }) - if (user.deletedAt) throw new Error('This user was deleted. Cannot confirm a contribution.') - + if (user.deletedAt) { + logger.error('This user was deleted. Cannot confirm a contribution.') + throw new Error('This user was deleted. Cannot confirm a contribution.') + } const creations = await getUserCreation(contribution.userId, false) validateContribution(creations, contribution.amount, contribution.contributionDate) @@ -501,6 +526,18 @@ export class AdminResolver { @Mutation(() => Boolean) async sendActivationEmail(@Arg('email') email: string): Promise { email = email.trim().toLowerCase() + const emailContact = await UserContact.findOne({ email: email }) + if (!emailContact) { + logger.error(`Could not find UserContact with email: ${email}`) + throw new Error(`Could not find UserContact with email: ${email}`) + } + const user = await dbUser.findOne({ id: emailContact.userId }) + if (!user) { + logger.error(`Could not find User to emailContact: ${email}`) + throw new Error(`Could not find User to emailContact: ${email}`) + } + + /* const user = await dbUser.findOneOrFail({ email: email }) // can be both types: REGISTER and RESET_PASSWORD @@ -510,23 +547,21 @@ export class AdminResolver { }) optInCode = await checkOptInCode(optInCode, user) + */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ - link: activationLink(optInCode), + link: activationLink(emailContact.emailVerificationCode), firstName: user.firstName, lastName: user.lastName, email, duration: printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME), }) - /* uncomment this, when you need the activation link on the console // In case EMails are disabled log the activation link for the user if (!emailSent) { - // eslint-disable-next-line no-console - console.log(`Account confirmation link: ${activationLink}`) + logger.info(`Account confirmation link: ${activationLink}`) } - */ return true } diff --git a/backend/src/graphql/resolver/GdtResolver.ts b/backend/src/graphql/resolver/GdtResolver.ts index 56a95c9f0..a1d75e946 100644 --- a/backend/src/graphql/resolver/GdtResolver.ts +++ b/backend/src/graphql/resolver/GdtResolver.ts @@ -20,7 +20,7 @@ export class GdtResolver { try { const resultGDT = await apiGet( - `${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.email}/${currentPage}/${pageSize}/${order}`, + `${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`, ) if (!resultGDT.success) { throw new Error(resultGDT.data) @@ -37,7 +37,7 @@ export class GdtResolver { const user = getUser(context) try { const resultGDTSum = await apiPost(`${CONFIG.GDT_API_URL}/GdtEntries/sumPerEmailApi`, { - email: user.email, + email: user.emailContact.email, }) if (!resultGDTSum.success) { throw new Error('Call not successful') diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index bc062a1f4..ae6445343 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -35,6 +35,7 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' +import { UserContact } from '@entity/UserContact' export const executeTransaction = async ( amount: Decimal, @@ -148,8 +149,8 @@ export const executeTransaction = async ( senderLastName: sender.lastName, recipientFirstName: recipient.firstName, recipientLastName: recipient.lastName, - email: recipient.email, - senderEmail: sender.email, + email: recipient.emailContact.email, + senderEmail: sender.emailContact.email, amount, memo, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, @@ -171,7 +172,7 @@ export class TransactionResolver { const user = getUser(context) logger.addContext('user', user.id) - logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.email})`) + logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`) // find current balance const lastTransaction = await dbTransaction.findOne( @@ -293,16 +294,22 @@ export class TransactionResolver { } // validate recipient user - const recipientUser = await dbUser.findOne({ email: email }, { withDeleted: true }) + const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) + if (!emailContact) { + logger.error(`Could not find UserContact with email: ${email}`) + throw new Error(`Could not find UserContact with email: ${email}`) + } + + const recipientUser = await dbUser.findOne({ id: emailContact.userId }) if (!recipientUser) { - logger.error(`recipient not known: email=${email}`) - throw new Error('recipient not known') + logger.error(`unknown recipient to UserContact: email=${email}`) + throw new Error('unknown recipient') } if (recipientUser.deletedAt) { logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) throw new Error('The recipient account was deleted') } - if (!recipientUser.emailChecked) { + if (!emailContact.emailChecked) { logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`) throw new Error('The recipient account is not activated') } diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 9aba4d6b1..37a9946a7 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -29,6 +29,7 @@ import { EventLogin, EventRedeemRegister, EventRegister, + EventSendAccountMultiRegistrationEmail, EventSendConfirmationEmail, } from '@/event/Event' import { getUserCreation } from './util/creations' @@ -417,50 +418,55 @@ export class UserResolver { ) // TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // default int publisher_id = 0; + const event = new Event() // Validate Language (no throw) if (!language || !isLanguage(language)) { language = DEFAULT_LANGUAGE } - // Validate email unique + // check if user with email still exists? email = email.trim().toLowerCase() - const foundUser = await findUserByEmail(email) + if (await checkEmailExists(email)) { + const foundUser = await findUserByEmail(email) + logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) - // TODO we cannot use repository.count(), since it does not allow to specify if you want to include the soft deletes - // const userFound = await DbUser.findOne({ email }, { withDeleted: true }) - logger.info(`DbUser.findOne(email=${email}) = ${foundUser}`) + if (foundUser) { + // ATTENTION: this logger-message will be exactly expected during tests + logger.info(`User already exists with this email=${email}`) + // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. - if (foundUser) { - // ATTENTION: this logger-message will be exactly expected during tests - logger.info(`User already exists with this email=${email}`) - // TODO: this is unsecure, but the current implementation of the login server. This way it can be queried if the user with given EMail is existent. + const user = new User(communityDbUser) + user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in? + user.gradidoID = uuidv4() + user.email = email + user.firstName = firstName + user.lastName = lastName + user.language = language + user.publisherId = publisherId + logger.debug('partly faked user=' + user) - const user = new User(communityDbUser) - user.id = sodium.randombytes_random() % (2048 * 16) // TODO: for a better faking derive id from email so that it will be always the same id when the same email comes in? - user.gradidoID = uuidv4() - user.email = email - user.firstName = firstName - user.lastName = lastName - user.language = language - user.publisherId = publisherId - logger.debug('partly faked user=' + user) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const emailSent = await sendAccountMultiRegistrationEmail({ + firstName, + lastName, + email, + }) + const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail() + eventSendAccountMultiRegistrationEmail.userId = foundUser.id + eventProtocol.writeEvent( + event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail), + ) + logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`) + /* uncomment this, when you need the activation link on the console */ + // In case EMails are disabled log the activation link for the user + if (!emailSent) { + logger.debug(`Email not send!`) + } + logger.info('createUser() faked and send multi registration mail...') - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendAccountMultiRegistrationEmail({ - firstName, - lastName, - email, - }) - logger.info(`sendAccountMultiRegistrationEmail of ${firstName}.${lastName} to ${email}`) - /* uncomment this, when you need the activation link on the console */ - // In case EMails are disabled log the activation link for the user - if (!emailSent) { - logger.debug(`Email not send!`) + return user } - logger.info('createUser() faked and send multi registration mail...') - - return user } const passphrase = PassphraseGenerate() @@ -473,16 +479,11 @@ export class UserResolver { const eventRegister = new EventRegister() const eventRedeemRegister = new EventRedeemRegister() const eventSendConfirmEmail = new EventSendConfirmationEmail() - // const dbEmailContact = new DbUserContact() - // dbEmailContact.email = email - const dbUser = new DbUser() - // dbUser.emailContact = dbEmailContact + let dbUser = new DbUser() dbUser.gradidoID = gradidoID - // dbUser.email = email dbUser.firstName = firstName dbUser.lastName = lastName - // dbUser.emailHash = emailHash dbUser.language = language dbUser.publisherId = publisherId dbUser.passphrase = passphrase.join(' ') @@ -513,22 +514,22 @@ export class UserResolver { // loginUser.pubKey = keyPair[0] // loginUser.privKey = encryptedPrivkey - const event = new Event() const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('READ UNCOMMITTED') try { - await queryRunner.manager.save(dbUser).catch((error) => { + dbUser = await queryRunner.manager.save(dbUser).catch((error) => { logger.error('Error while saving dbUser', error) throw new Error('error saving user') }) - const emailContact = newEmailContact(email, dbUser.id) - await queryRunner.manager.save(emailContact).catch((error) => { + let emailContact = newEmailContact(email, dbUser.id) + emailContact = await queryRunner.manager.save(emailContact).catch((error) => { logger.error('Error while saving emailContact', error) throw new Error('error saving email user contact') }) dbUser.emailContact = emailContact + dbUser.emailId = emailContact.id await queryRunner.manager.save(dbUser).catch((error) => { logger.error('Error while updating dbUser', error) throw new Error('error updating user') @@ -559,8 +560,6 @@ export class UserResolver { eventSendConfirmEmail.userId = dbUser.id eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail)) - /* uncomment this, when you need the activation link on the console */ - // In case EMails are disabled log the activation link for the user if (!emailSent) { logger.debug(`Account confirmation link: ${activationLink}`) } @@ -893,20 +892,30 @@ export class UserResolver { } async function findUserByEmail(email: string): Promise { - const dbUserContact = await DbUserContact.findOneOrFail(email, { withDeleted: true }).catch( - () => { - logger.error(`UserContact with email=${email} does not exists`) - throw new Error('No user with this credentials') - }, - ) - const userId = dbUserContact.userId - const dbUser = await DbUser.findOneOrFail(userId).catch(() => { - logger.error(`User with emeilContact=${email} connected per userId=${userId} does not exist`) + const dbUserContact = await DbUserContact.findOneOrFail( + { email: email }, + { withDeleted: true }, + ).catch(() => { + logger.error(`UserContact with email=${email} does not exists`) throw new Error('No user with this credentials') }) + const userId = dbUserContact.userId + const dbUser = await DbUser.findOneOrFail(userId).catch(() => { + logger.error(`User with emailContact=${email} connected per userId=${userId} does not exist`) + throw new Error('No user with this credentials') + }) + dbUser.emailContact = dbUserContact return dbUser } +async function checkEmailExists(email: string): Promise { + const userContact = await DbUserContact.findOne({ email: email }, { withDeleted: true }) + if (userContact) { + return true + } + return false +} + /* const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => { const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime() diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 01f61dcbc..21cafbb30 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -4,9 +4,15 @@ import { User } from '@entity/User' @EntityRepository(User) export class UserRepository extends Repository { async findByPubkeyHex(pubkeyHex: string): Promise { - return this.createQueryBuilder('user') + const user = await this.createQueryBuilder('user') .where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex }) .getOneOrFail() + /* + user.emailContact = await this.createQueryBuilder('userContact') + .where('userContact.id = :user.emailId', { user.emailId }) + .getOneOrFail() + */ + return user } async findBySearchCriteriaPagedFiltered( diff --git a/backend/src/util/communityUser.ts b/backend/src/util/communityUser.ts index 2f0743270..e885b7043 100644 --- a/backend/src/util/communityUser.ts +++ b/backend/src/util/communityUser.ts @@ -12,6 +12,7 @@ const communityDbUser: dbUser = { alias: '', // email: 'support@gradido.net', emailContact: new UserContact(), + emailId: -1, firstName: 'Gradido', lastName: 'Akademie', pubKey: Buffer.from(''), diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts index d5eaef521..6c8ca7e49 100644 --- a/backend/src/webhook/elopage.ts +++ b/backend/src/webhook/elopage.ts @@ -30,6 +30,7 @@ import { LoginElopageBuys } from '@entity/LoginElopageBuys' import { UserResolver } from '@/graphql/resolver/UserResolver' import { User as dbUser } from '@entity/User' +import { UserContact as dbUserContact } from '@entity/UserContact' export const elopageWebhook = async (req: any, res: any): Promise => { // eslint-disable-next-line no-console @@ -127,7 +128,8 @@ export const elopageWebhook = async (req: any, res: any): Promise => { } // Do we already have such a user? - if ((await dbUser.count({ email })) !== 0) { + // if ((await dbUser.count({ email })) !== 0) { + if ((await dbUserContact.count({ email })) !== 0) { // eslint-disable-next-line no-console console.log(`Did not create User - already exists with email: ${email}`) return diff --git a/database/entity/0048-add_user_contacts_table/User.ts b/database/entity/0048-add_user_contacts_table/User.ts index 010cb0c20..40bfa601a 100644 --- a/database/entity/0048-add_user_contacts_table/User.ts +++ b/database/entity/0048-add_user_contacts_table/User.ts @@ -43,12 +43,12 @@ export class User extends BaseEntity { @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) email: string */ - @OneToOne(() => UserContact, { primary: true, cascade: true }) + @OneToOne(() => UserContact) @JoinColumn({ name: 'email_id' }) emailContact: UserContact @Column({ name: 'email_id', type: 'int', unsigned: true, nullable: true, default: null }) - emailId?: number | null + emailId: number | null @Column({ name: 'first_name', diff --git a/database/entity/0048-add_user_contacts_table/UserContact.ts b/database/entity/0048-add_user_contacts_table/UserContact.ts index 7c2dff3db..936e433a6 100644 --- a/database/entity/0048-add_user_contacts_table/UserContact.ts +++ b/database/entity/0048-add_user_contacts_table/UserContact.ts @@ -1,4 +1,13 @@ -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + DeleteDateColumn, + OneToOne, + JoinColumn, +} from 'typeorm' +import { User } from './User' @Entity('user_contacts', { engine: 'InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci' }) export class UserContact extends BaseEntity { @@ -14,6 +23,10 @@ export class UserContact extends BaseEntity { }) type: string + @OneToOne(() => User) + @JoinColumn({ name: 'user_id' }) + user: User + @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false }) userId: number diff --git a/database/migrations/0048-add_user_contacts_table.ts b/database/migrations/0048-add_user_contacts_table.ts index 49f647e39..d1b35a400 100644 --- a/database/migrations/0048-add_user_contacts_table.ts +++ b/database/migrations/0048-add_user_contacts_table.ts @@ -22,7 +22,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, \`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - \`updated_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + \`updated_at\` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, \`deleted_at\` datetime NULL DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) From 0f90f960ceb83570ed00b3609b73740c94052c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 26 Aug 2022 02:40:44 +0200 Subject: [PATCH 28/92] remove unused code --- backend/src/typeorm/repository/User.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 21cafbb30..04a30de8f 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -4,15 +4,9 @@ import { User } from '@entity/User' @EntityRepository(User) export class UserRepository extends Repository { async findByPubkeyHex(pubkeyHex: string): Promise { - const user = await this.createQueryBuilder('user') + return await this.createQueryBuilder('user') .where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex }) .getOneOrFail() - /* - user.emailContact = await this.createQueryBuilder('userContact') - .where('userContact.id = :user.emailId', { user.emailId }) - .getOneOrFail() - */ - return user } async findBySearchCriteriaPagedFiltered( From 1184666fe2062694d3a96c397181fdeb8d2a1fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 26 Aug 2022 14:42:37 +0200 Subject: [PATCH 29/92] try to solve problem of dbUser-entity with emailContact in context --- backend/src/graphql/directive/isAuthorized.ts | 2 +- backend/src/typeorm/repository/User.ts | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/directive/isAuthorized.ts b/backend/src/graphql/directive/isAuthorized.ts index 065c01957..c24cde47a 100644 --- a/backend/src/graphql/directive/isAuthorized.ts +++ b/backend/src/graphql/directive/isAuthorized.ts @@ -31,7 +31,7 @@ const isAuthorized: AuthChecker = async ({ context }, rights) => { // TODO - load from database dynamically & admin - maybe encode this in the token to prevent many database requests // TODO this implementation is bullshit - two database queries cause our user identifiers are not aligned and vary between email, id and pubKey - const userRepository = await getCustomRepository(UserRepository) + const userRepository = getCustomRepository(UserRepository) try { const user = await userRepository.findByPubkeyHex(context.pubKey) context.user = user diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 04a30de8f..3c859ce0c 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -1,12 +1,20 @@ import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm' -import { User } from '@entity/User' +import { User as DbUser } from '@entity/User' -@EntityRepository(User) -export class UserRepository extends Repository { - async findByPubkeyHex(pubkeyHex: string): Promise { - return await this.createQueryBuilder('user') +@EntityRepository(DbUser) +export class UserRepository extends Repository { + async findByPubkeyHex(pubkeyHex: string): Promise { + const dbUser = await this.createQueryBuilder('user') .where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex }) .getOneOrFail() + /* + const dbUser = await this.findOneOrFail(`hex(user.pubKey) = { pubkeyHex }`) + const emailContact = await this.query( + `SELECT * from user_contacts where id = { dbUser.emailId }`, + ) + dbUser.emailContact = emailContact + */ + return dbUser } async findBySearchCriteriaPagedFiltered( @@ -15,7 +23,7 @@ export class UserRepository extends Repository { filterCriteria: ObjectLiteral[], currentPage: number, pageSize: number, - ): Promise<[User[], number]> { + ): Promise<[DbUser[], number]> { const query = await this.createQueryBuilder('user') .select(select) .withDeleted() From 1d1de2011a1f2bfc3691382da53f434232b6d877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 26 Aug 2022 20:00:29 +0200 Subject: [PATCH 30/92] failed try to load OneToOne relation of user to userConmtact as emailConact-attribut... grrrrrr... --- backend/src/graphql/model/User.ts | 8 +++++--- backend/src/typeorm/repository/User.ts | 3 ++- database/entity/0048-add_user_contacts_table/User.ts | 2 +- .../entity/0048-add_user_contacts_table/UserContact.ts | 3 +-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index a28fe4b69..e64df8294 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -12,13 +12,15 @@ export class User { this.gradidoID = user.gradidoID this.alias = user.alias this.emailId = user.emailId - this.email = user.emailContact.email - this.emailContact = user.emailContact + if (user.emailContact) { + this.email = user.emailContact.email + this.emailContact = user.emailContact + this.emailChecked = user.emailContact.emailChecked + } this.firstName = user.firstName this.lastName = user.lastName this.deletedAt = user.deletedAt this.createdAt = user.createdAt - this.emailChecked = user.emailContact.emailChecked this.language = user.language this.publisherId = user.publisherId this.isAdmin = user.isAdmin diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index 3c859ce0c..b347fae40 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -5,6 +5,7 @@ import { User as DbUser } from '@entity/User' export class UserRepository extends Repository { async findByPubkeyHex(pubkeyHex: string): Promise { const dbUser = await this.createQueryBuilder('user') + .leftJoinAndSelect('user.emailContact', 'emailContact') .where('hex(user.pubKey) = :pubkeyHex', { pubkeyHex }) .getOneOrFail() /* @@ -24,7 +25,7 @@ export class UserRepository extends Repository { currentPage: number, pageSize: number, ): Promise<[DbUser[], number]> { - const query = await this.createQueryBuilder('user') + const query = this.createQueryBuilder('user') .select(select) .withDeleted() .where( diff --git a/database/entity/0048-add_user_contacts_table/User.ts b/database/entity/0048-add_user_contacts_table/User.ts index 40bfa601a..6c4bf52f1 100644 --- a/database/entity/0048-add_user_contacts_table/User.ts +++ b/database/entity/0048-add_user_contacts_table/User.ts @@ -43,7 +43,7 @@ export class User extends BaseEntity { @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) email: string */ - @OneToOne(() => UserContact) + @OneToOne(() => UserContact, (emailContact) => emailContact.userId) @JoinColumn({ name: 'email_id' }) emailContact: UserContact diff --git a/database/entity/0048-add_user_contacts_table/UserContact.ts b/database/entity/0048-add_user_contacts_table/UserContact.ts index 936e433a6..942a7de4f 100644 --- a/database/entity/0048-add_user_contacts_table/UserContact.ts +++ b/database/entity/0048-add_user_contacts_table/UserContact.ts @@ -23,8 +23,7 @@ export class UserContact extends BaseEntity { }) type: string - @OneToOne(() => User) - @JoinColumn({ name: 'user_id' }) + @OneToOne(() => User, (user) => user.emailContact) user: User @Column({ name: 'user_id', type: 'int', unsigned: true, nullable: false }) From 7269b4b67b1eb46b2c3e978ab30ca063ab807609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 30 Aug 2022 00:57:57 +0200 Subject: [PATCH 31/92] solve problems reading user with emailcontact --- backend/src/graphql/model/User.ts | 2 +- backend/src/graphql/model/UserContact.ts | 4 ++-- backend/src/graphql/resolver/UserResolver.ts | 6 +----- database/entity/0048-add_user_contacts_table/User.ts | 4 ++-- database/entity/0048-add_user_contacts_table/UserContact.ts | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index e64df8294..8d56c6775 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -14,7 +14,7 @@ export class User { this.emailId = user.emailId if (user.emailContact) { this.email = user.emailContact.email - this.emailContact = user.emailContact + this.emailContact = new UserContact(user.emailContact) this.emailChecked = user.emailContact.emailChecked } this.firstName = user.firstName diff --git a/backend/src/graphql/model/UserContact.ts b/backend/src/graphql/model/UserContact.ts index 902e2f9f2..fda79559f 100644 --- a/backend/src/graphql/model/UserContact.ts +++ b/backend/src/graphql/model/UserContact.ts @@ -1,9 +1,9 @@ import { ObjectType, Field } from 'type-graphql' -import { UserContact as dbUserCOntact} from '@entity/UserContact' +import { UserContact as dbUserContact} from '@entity/UserContact' @ObjectType() export class UserContact { - constructor(userContact: dbUserCOntact) { + constructor(userContact: dbUserContact) { this.id = userContact.id this.type = userContact.type this.userId = userContact.userId diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 37a9946a7..2ed77dbdd 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -850,11 +850,7 @@ export class UserResolver { @Query(() => Boolean) async hasElopage(@Ctx() context: Context): Promise { logger.info(`hasElopage()...`) - const userEntity = context.user - if (!userEntity) { - logger.info('missing context.user for EloPage-check') - return false - } + const userEntity = getUser(context) const elopageBuys = hasElopageBuys(userEntity.emailContact.email) logger.debug(`has ElopageBuys = ${elopageBuys}`) return elopageBuys diff --git a/database/entity/0048-add_user_contacts_table/User.ts b/database/entity/0048-add_user_contacts_table/User.ts index 6c4bf52f1..ab79c8ffa 100644 --- a/database/entity/0048-add_user_contacts_table/User.ts +++ b/database/entity/0048-add_user_contacts_table/User.ts @@ -43,7 +43,7 @@ export class User extends BaseEntity { @Column({ length: 255, unique: true, nullable: false, collation: 'utf8mb4_unicode_ci' }) email: string */ - @OneToOne(() => UserContact, (emailContact) => emailContact.userId) + @OneToOne(() => UserContact, (emailContact) => emailContact.user) @JoinColumn({ name: 'email_id' }) emailContact: UserContact @@ -115,7 +115,7 @@ export class User extends BaseEntity { @JoinColumn({ name: 'user_id' }) contributions?: Contribution[] - @OneToMany(() => UserContact, (usercontact) => usercontact.userId) + @OneToMany(() => UserContact, (usercontact) => usercontact.user) @JoinColumn({ name: 'user_id' }) usercontacts?: UserContact[] } diff --git a/database/entity/0048-add_user_contacts_table/UserContact.ts b/database/entity/0048-add_user_contacts_table/UserContact.ts index 942a7de4f..20732ae6f 100644 --- a/database/entity/0048-add_user_contacts_table/UserContact.ts +++ b/database/entity/0048-add_user_contacts_table/UserContact.ts @@ -53,7 +53,7 @@ export class UserContact extends BaseEntity { @Column({ name: 'created_at', default: () => 'CURRENT_TIMESTAMP', nullable: false }) createdAt: Date - @DeleteDateColumn({ name: 'updated_at', nullable: true }) + @Column({ name: 'updated_at', nullable: true, default: null, type: 'datetime' }) updatedAt: Date | null @DeleteDateColumn({ name: 'deleted_at', nullable: true }) From f5ee1614f8ddeacf86134985a391a1fba2ea0d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 31 Aug 2022 01:06:59 +0200 Subject: [PATCH 32/92] add precision of 3 milliseconds on datetime collumns --- .../0048-add_user_contacts_table.ts | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/database/migrations/0048-add_user_contacts_table.ts b/database/migrations/0048-add_user_contacts_table.ts index d1b35a400..f2a436e53 100644 --- a/database/migrations/0048-add_user_contacts_table.ts +++ b/database/migrations/0048-add_user_contacts_table.ts @@ -10,6 +10,7 @@ import { v4 as uuidv4 } from 'uuid' export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` CREATE TABLE IF NOT EXISTS \`user_contacts\` ( \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, @@ -21,13 +22,25 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`email_resend_count\` int DEFAULT '0', \`email_checked\` tinyint(4) NOT NULL DEFAULT 0, \`phone\` varchar(255) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, - \`created_at\` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, - \`updated_at\` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP, - \`deleted_at\` datetime NULL DEFAULT NULL, + \`created_at\` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + \`updated_at\` datetime(3) NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(3), + \`deleted_at\` datetime(3) NULL DEFAULT NULL, PRIMARY KEY (\`id\`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) await queryFn('ALTER TABLE `users` ADD COLUMN `email_id` int(10) NULL AFTER `email`;') + // define datetime column with a precision of 3 milliseconds + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `created` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) AFTER `email_hash`;', + ) + // define datetime column with a precision of 3 milliseconds + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `deletedAt` datetime(3) NULL DEFAULT NULL AFTER `last_name`;', + ) + // define datetime column with a precision of 3 milliseconds + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `is_admin` datetime(3) NULL DEFAULT NULL AFTER `language`;', + ) // merge values from login_email_opt_in table with users.email in new user_contacts table await queryFn(` @@ -58,13 +71,26 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis `UPDATE users as u SET u.email_id = "${contact.id}" WHERE u.id = "${contact.user_id}"`, ) } - // this step comes after verification and test + // these steps comes after verification and test await queryFn('ALTER TABLE users DROP COLUMN email;') + await queryFn('ALTER TABLE users DROP COLUMN email_checked;') } export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { // this step comes after verification and test await queryFn('ALTER TABLE users ADD COLUMN email varchar(255) NULL AFTER privkey;') + await queryFn( + 'ALTER TABLE users ADD COLUMN email_checked tinyint(4) NOT NULL DEFAULT 0 AFTER email;', + ) + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `created` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `email_hash`;', + ) + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `deletedAt` datetime NULL DEFAULT NULL AFTER `last_name`;', + ) + await queryFn( + 'ALTER TABLE `users` MODIFY COLUMN `is_admin` datetime NULL DEFAULT NULL AFTER `language`;', + ) // reconstruct the previous email back from contacts to users table const contacts = await queryFn(`SELECT c.id, c.email, c.user_id FROM user_contacts as c`) From 852c4e64992f052bea193eb306ec0aa348a9e80f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 31 Aug 2022 01:12:12 +0200 Subject: [PATCH 33/92] change seeding now with UserContacts entry --- backend/src/graphql/resolver/UserResolver.ts | 19 ++++++++++++------- backend/src/seeds/factory/user.ts | 19 ++++++++++++++----- backend/src/seeds/index.ts | 11 ++++++++++- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 2ed77dbdd..6a59ee22f 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -371,7 +371,7 @@ export class UserResolver { logger.debug('login credentials valid...') const user = new User(dbUser, await getUserCreation(dbUser.id)) - logger.debug('user=' + user) + logger.debug(`user= ${JSON.stringify(user, null, 2)}`) // Elopage Status & Stored PublisherId user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) @@ -389,7 +389,7 @@ export class UserResolver { const ev = new EventLogin() ev.userId = user.id eventProtocol.writeEvent(new Event().setEventLogin(ev)) - logger.info('successful Login:' + user) + logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) return user } @@ -665,14 +665,14 @@ export class UserResolver { `email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`, ) } - logger.debug('optInCode is valid...') + logger.debug('EmailVerificationCode is valid...') // load user const user = await DbUser.findOneOrFail({ id: userContact.userId }).catch(() => { logger.error('Could not find corresponding Login User') throw new Error('Could not find corresponding Login User') }) - logger.debug('user with optInCode found...') + logger.debug('user with EmailVerificationCode found...') // Generate Passphrase if needed if (!user.passphrase) { @@ -713,12 +713,17 @@ export class UserResolver { logger.error('error saving user: ' + error) throw new Error('error saving user: ' + error) }) + // Save userContact + await queryRunner.manager.save(userContact).catch((error) => { + logger.error('error saving userContact: ' + error) + throw new Error('error saving userContact: ' + error) + }) await queryRunner.commitTransaction() - logger.info('User data written successfully...') + logger.info('User and UserContact data written successfully...') } catch (e) { await queryRunner.rollbackTransaction() - logger.error('Error on writing User data:' + e) + logger.error('Error on writing User and UserContact data:' + e) throw e } finally { await queryRunner.release() @@ -896,7 +901,7 @@ async function findUserByEmail(email: string): Promise { throw new Error('No user with this credentials') }) const userId = dbUserContact.userId - const dbUser = await DbUser.findOneOrFail(userId).catch(() => { + const dbUser = await DbUser.findOneOrFail({ id: userId }, { withDeleted: true }).catch(() => { logger.error(`User with emailContact=${email} connected per userId=${userId} does not exist`) throw new Error('No user with this credentials') }) diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index d94f94b3c..c2eb20bc4 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -1,8 +1,8 @@ import { createUser, setPassword } from '@/seeds/graphql/mutations' import { User } from '@entity/User' -import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { UserInterface } from '@/seeds/users/UserInterface' import { ApolloServerTestClient } from 'apollo-server-testing' +import { UserContact } from '@entity/UserContact' export const userFactory = async ( client: ApolloServerTestClient, @@ -15,17 +15,23 @@ export const userFactory = async ( createUser: { id }, }, } = await mutate({ mutation: createUser, variables: user }) + // console.log('creatUser:', { id }, { user }) + // get user from database + let dbUser = await User.findOneOrFail({ id }) + // console.log('dbUser:', dbUser) + + const emailContact = await UserContact.findOneOrFail({ userId: id }) + // console.log('emailContact:', emailContact) if (user.emailChecked) { - const optin = await LoginEmailOptIn.findOneOrFail({ userId: id }) await mutate({ mutation: setPassword, - variables: { password: 'Aa12345_', code: optin.verificationCode }, + variables: { password: 'Aa12345_', code: emailContact.emailVerificationCode }, }) } - // get user from database - const dbUser = await User.findOneOrFail({ id }) + // get last changes of user from database + dbUser = await User.findOneOrFail({ id }) if (user.createdAt || user.deletedAt || user.isAdmin) { if (user.createdAt) dbUser.createdAt = user.createdAt @@ -34,5 +40,8 @@ export const userFactory = async ( await dbUser.save() } + // get last changes of user from database + dbUser = await User.findOneOrFail({ id }, { withDeleted: true }) + return dbUser } diff --git a/backend/src/seeds/index.ts b/backend/src/seeds/index.ts index 8e9a4e2d8..c5a55cb84 100644 --- a/backend/src/seeds/index.ts +++ b/backend/src/seeds/index.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { backendLogger as logger } from '@/server/logger' import createServer from '../server/createServer' import { createTestClient } from 'apollo-server-testing' @@ -50,11 +51,14 @@ const run = async () => { const seedClient = createTestClient(server.apollo) const { con } = server await cleanDB() + logger.info('##seed## clean database successful...') // seed the standard users for (let i = 0; i < users.length; i++) { - await userFactory(seedClient, users[i]) + const dbUser = await userFactory(seedClient, users[i]) + logger.info(`##seed## seed standard users[ ${i} ]= ${JSON.stringify(dbUser, null, 2)}`) } + logger.info('##seed## seeding all standard users successful...') // seed 100 random users for (let i = 0; i < 100; i++) { @@ -64,7 +68,9 @@ const run = async () => { email: internet.email(), language: datatype.boolean() ? 'en' : 'de', }) + logger.info(`##seed## seed ${i}. random user`) } + logger.info('##seed## seeding all random users successful...') // create GDD for (let i = 0; i < creations.length; i++) { @@ -73,16 +79,19 @@ const run = async () => { // eslint-disable-next-line no-empty while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886) } + logger.info('##seed## seeding all creations successful...') // create Transaction Links for (let i = 0; i < transactionLinks.length; i++) { await transactionLinkFactory(seedClient, transactionLinks[i]) } + logger.info('##seed## seeding all transactionLinks successful...') // create Contribution Links for (let i = 0; i < contributionLinks.length; i++) { await contributionLinkFactory(seedClient, contributionLinks[i]) } + logger.info('##seed## seeding all contributionLinks successful...') await con.close() } From faa0500f100f2ba64b5d71c165610df75bbe818d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 31 Aug 2022 15:44:41 +0200 Subject: [PATCH 34/92] adapt seeding using User and UserContact --- backend/src/graphql/model/UserContact.ts | 2 +- backend/src/graphql/resolver/AdminResolver.ts | 9 ++++-- backend/src/graphql/resolver/UserResolver.ts | 7 +++-- .../src/graphql/resolver/util/creations.ts | 14 +++++++-- backend/src/seeds/factory/creation.ts | 29 +++++++++++++++++-- 5 files changed, 50 insertions(+), 11 deletions(-) diff --git a/backend/src/graphql/model/UserContact.ts b/backend/src/graphql/model/UserContact.ts index fda79559f..796c7f5f3 100644 --- a/backend/src/graphql/model/UserContact.ts +++ b/backend/src/graphql/model/UserContact.ts @@ -1,5 +1,5 @@ import { ObjectType, Field } from 'type-graphql' -import { UserContact as dbUserContact} from '@entity/UserContact' +import { UserContact as dbUserContact } from '@entity/UserContact' @ObjectType() export class UserContact { diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 5d283026d..7fde128c9 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -32,7 +32,6 @@ import { TransactionRepository } from '@repository/Transaction' import { calculateDecay } from '@/util/decay' import { Contribution } from '@entity/Contribution' import { hasElopageBuys } from '@/util/hasElopageBuys' -import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User as dbUser } from '@entity/User' import { User } from '@model/User' import { TransactionTypeId } from '@enum/TransactionTypeId' @@ -44,7 +43,7 @@ import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' -import { checkEmailVerificationCode, activationLink, printTimeDuration } from './UserResolver' +import { activationLink, printTimeDuration } from './UserResolver' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -247,6 +246,9 @@ export class AdminResolver { @Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs, @Ctx() context: Context, ): Promise { + logger.info( + `adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`, + ) const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) if (!emailContact) { logger.error(`Could not find user with email: ${email}`) @@ -263,8 +265,9 @@ export class AdminResolver { const moderator = getUser(context) logger.trace('moderator: ', moderator.id) const creations = await getUserCreation(emailContact.userId) - logger.trace('creations', creations) + logger.trace('creations:', creations) const creationDateObj = new Date(creationDate) + logger.trace('creationDateObj:', creationDateObj) validateContribution(creations, amount, creationDateObj) const contribution = Contribution.create() contribution.userId = emailContact.userId diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 6a59ee22f..514c52973 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -16,7 +16,6 @@ import UnsecureLoginArgs from '@arg/UnsecureLoginArgs' import UpdateUserInfosArgs from '@arg/UpdateUserInfosArgs' import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddleware' import { OptInType } from '@enum/OptInType' -import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { sendResetPasswordEmail as sendResetPasswordEmailMailer } from '@/mailer/sendResetPasswordEmail' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegistrationEmail' @@ -148,6 +147,7 @@ const SecretKeyCryptographyCreateKey = (salt: string, password: string): Buffer[ return [encryptionKeyHash, encryptionKey] } +/* const getEmailHash = (email: string): Buffer => { logger.trace('getEmailHash...') const emailHash = Buffer.alloc(sodium.crypto_generichash_BYTES) @@ -155,6 +155,7 @@ const getEmailHash = (email: string): Buffer => { logger.debug(`getEmailHash...successful: ${emailHash}`) return emailHash } +*/ const SecretKeyCryptographyEncrypt = (message: Buffer, encryptionKey: Buffer): Buffer => { logger.trace('SecretKeyCryptographyEncrypt...') @@ -191,7 +192,7 @@ const newEmailContact = (email: string, userId: number): DbUserContact => { logger.debug(`newEmailContact...successful: ${emailContact}`) return emailContact } - +/* const newEmailOptIn = (userId: number): LoginEmailOptIn => { logger.trace('newEmailOptIn...') const emailOptIn = new LoginEmailOptIn() @@ -201,7 +202,7 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => { logger.debug(`newEmailOptIn...successful: ${emailOptIn}`) return emailOptIn } - +*/ /* // needed by AdminResolver // checks if given code exists and can be resent diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index ad15ebec6..4f1cec0e0 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -15,14 +15,21 @@ export const validateContribution = ( amount: Decimal, creationDate: Date, ): void => { - logger.trace('isContributionValid', creations, amount, creationDate) + logger.trace('isContributionValid: ', creations, amount, creationDate) const index = getCreationIndex(creationDate.getMonth()) if (index < 0) { + logger.error( + 'No information for available creations with the given creationDate=', + creationDate, + ) throw new Error('No information for available creations for the given date') } if (amount.greaterThan(creations[index].toString())) { + logger.error( + `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, + ) throw new Error( `The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`, ) @@ -41,7 +48,7 @@ export const getUserCreations = async ( await queryRunner.connect() const dateFilter = 'last_day(curdate() - interval 3 month) + interval 1 day' - logger.trace('getUserCreations dateFilter', dateFilter) + logger.trace('getUserCreations dateFilter=', dateFilter) const unionString = includePending ? ` @@ -51,6 +58,7 @@ export const getUserCreations = async ( AND contribution_date >= ${dateFilter} AND confirmed_at IS NULL AND deleted_at IS NULL` : '' + logger.trace('getUserCreations unionString=', unionString) const unionQuery = await queryRunner.manager.query(` SELECT MONTH(date) AS month, sum(amount) AS sum, userId AS id FROM @@ -62,6 +70,7 @@ export const getUserCreations = async ( GROUP BY month, userId ORDER BY date DESC `) + logger.trace('getUserCreations unionQuery=', unionQuery) await queryRunner.release() @@ -82,6 +91,7 @@ export const getUserCreations = async ( export const getUserCreation = async (id: number, includePending = true): Promise => { logger.trace('getUserCreation', id, includePending) const creations = await getUserCreations([id], includePending) + logger.trace('getUserCreation creations=', creations) return creations[0] ? creations[0].creations : FULL_CREATION_AVAILABLE } diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index d3f0f78ca..05be6d28e 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { backendLogger as logger } from '@/server/logger' import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations' import { login } from '@/seeds/graphql/queries' import { CreationInterface } from '@/seeds/creation/CreationInterface' @@ -8,6 +9,7 @@ import { ApolloServerTestClient } from 'apollo-server-testing' import { User } from '@entity/User' import { Transaction } from '@entity/Transaction' import { Contribution } from '@entity/Contribution' +import { UserContact } from '@entity/UserContact' // import CONFIG from '@/config/index' export const nMonthsBefore = (date: Date, months = 1): string => { @@ -19,29 +21,46 @@ export const creationFactory = async ( creation: CreationInterface, ): Promise => { const { mutate, query } = client - + logger.trace('creationFactory...') await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } }) + logger.trace('creationFactory... after login') // TODO it would be nice to have this mutation return the id await mutate({ mutation: adminCreateContribution, variables: { ...creation } }) + logger.trace('creationFactory... after adminCreateContribution') - const user = await User.findOneOrFail({ where: { email: creation.email } }) + const userContact = await UserContact.findOneOrFail({ where: { email: creation.email } }) + logger.trace('creationFactory... after UserContact.findOneOrFail userContact=', userContact) + const user = await User.findOneOrFail({ where: { id: userContact.userId } }) + logger.trace('creationFactory... after User.findOneOrFail user=', user) const pendingCreation = await Contribution.findOneOrFail({ where: { userId: user.id, amount: creation.amount }, order: { createdAt: 'DESC' }, }) + logger.trace( + 'creationFactory... after Contribution.findOneOrFail pendingCreation=', + pendingCreation, + ) if (creation.confirmed) { + logger.trace('creationFactory... creation.confirmed=', creation.confirmed) await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } }) + logger.trace('creationFactory... after confirmContribution') const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id }) + logger.trace( + 'creationFactory... after Contribution.findOneOrFail confirmedCreation=', + confirmedCreation, + ) if (creation.moveCreationDate) { + logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate) const transaction = await Transaction.findOneOrFail({ where: { userId: user.id, creationDate: new Date(creation.creationDate) }, order: { balanceDate: 'DESC' }, }) + logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction) if (transaction.decay.equals(0) && transaction.creationDate) { confirmedCreation.contributionDate = new Date( nMonthsBefore(transaction.creationDate, creation.moveCreationDate), @@ -52,11 +71,17 @@ export const creationFactory = async ( transaction.balanceDate = new Date( nMonthsBefore(transaction.balanceDate, creation.moveCreationDate), ) + logger.trace('creationFactory... before transaction.save transaction=', transaction) await transaction.save() + logger.trace( + 'creationFactory... before confirmedCreation.save confirmedCreation=', + confirmedCreation, + ) await confirmedCreation.save() } } } else { + logger.trace('creationFactory... pendingCreation=', pendingCreation) return pendingCreation } } From eedaf9e6e3d75d63fcb57bf1dc208e30e5ef4ee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 31 Aug 2022 23:04:55 +0200 Subject: [PATCH 35/92] unit tests of AdminResolver now with Users and UserContacts --- .../graphql/resolver/AdminResolver.test.ts | 4 ++- backend/src/graphql/resolver/AdminResolver.ts | 36 +++++++++++++------ backend/src/seeds/factory/creation.ts | 12 +++---- backend/src/typeorm/repository/User.ts | 22 ++++++++++-- 4 files changed, 54 insertions(+), 20 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index f0ce064b4..9de1a7116 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1117,7 +1117,9 @@ describe('AdminResolver', () => { }), ).resolves.toEqual( expect.objectContaining({ - errors: [new GraphQLError('Could not find user with email: bob@baumeister.de')], + errors: [ + new GraphQLError('Could not find UserContact with email: bob@baumeister.de'), + ], }), ) }) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 7fde128c9..34819ae73 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -75,24 +75,24 @@ export class AdminResolver { { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs, ): Promise { const userRepository = getCustomRepository(UserRepository) - + /* const filterCriteria: ObjectLiteral[] = [] if (filters) { if (filters.byActivated !== null) { - filterCriteria.push({ emailChecked: filters.byActivated }) + filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated }) } if (filters.byDeleted !== null) { filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() }) } } - + */ const userFields = [ 'id', 'firstName', 'lastName', - 'email', - 'emailChecked', + 'emailId', + 'emailContact', 'deletedAt', 'isAdmin', ] @@ -101,7 +101,7 @@ export class AdminResolver { return 'user.' + fieldName }), searchText, - filterCriteria, + filters, currentPage, pageSize, ) @@ -249,7 +249,11 @@ export class AdminResolver { logger.info( `adminCreateContribution(email=${email}, amount=${amount}, memo=${memo}, creationDate=${creationDate})`, ) - const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) + const emailContact = await UserContact.findOne({ + where: { email }, + withDeleted: true, + relations: ['user'], + }) if (!emailContact) { logger.error(`Could not find user with email: ${email}`) throw new Error(`Could not find user with email: ${email}`) @@ -258,6 +262,10 @@ export class AdminResolver { logger.error('This emailContact was deleted. Cannot create a contribution.') throw new Error('This emailContact was deleted. Cannot create a contribution.') } + if (emailContact.user.deletedAt) { + logger.error('This user was deleted. Cannot create a contribution.') + throw new Error('This user was deleted. Cannot create a contribution.') + } if (!emailContact.emailChecked) { logger.error('Contribution could not be saved, Email is not activated') throw new Error('Contribution could not be saved, Email is not activated') @@ -317,12 +325,16 @@ export class AdminResolver { @Args() { id, email, amount, memo, creationDate }: AdminUpdateContributionArgs, @Ctx() context: Context, ): Promise { - const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) + const emailContact = await UserContact.findOne({ + where: { email }, + withDeleted: true, + relations: ['user'], + }) if (!emailContact) { logger.error(`Could not find UserContact with email: ${email}`) throw new Error(`Could not find UserContact with email: ${email}`) } - const user = await dbUser.findOne({ id: emailContact.userId }, { withDeleted: true }) + const user = emailContact.user if (!user) { logger.error(`Could not find User to emailContact: ${email}`) throw new Error(`Could not find User to emailContact: ${email}`) @@ -388,7 +400,11 @@ export class AdminResolver { const userIds = contributions.map((p) => p.userId) const userCreations = await getUserCreations(userIds) - const users = await dbUser.find({ where: { id: In(userIds) }, withDeleted: true }) + const users = await dbUser.find({ + where: { id: In(userIds) }, + withDeleted: true, + relations: ['emailContact'], + }) return contributions.map((contribution) => { const user = users.find((u) => u.id === contribution.userId) diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index 05be6d28e..7f19e2828 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -24,15 +24,16 @@ export const creationFactory = async ( logger.trace('creationFactory...') await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } }) logger.trace('creationFactory... after login') - // TODO it would be nice to have this mutation return the id await mutate({ mutation: adminCreateContribution, variables: { ...creation } }) logger.trace('creationFactory... after adminCreateContribution') - const userContact = await UserContact.findOneOrFail({ where: { email: creation.email } }) + const userContact = await UserContact.findOneOrFail({ + where: { email: creation.email }, + relations: ['user'], + }) logger.trace('creationFactory... after UserContact.findOneOrFail userContact=', userContact) - const user = await User.findOneOrFail({ where: { id: userContact.userId } }) - logger.trace('creationFactory... after User.findOneOrFail user=', user) + const user = userContact.user const pendingCreation = await Contribution.findOneOrFail({ where: { userId: user.id, amount: creation.amount }, @@ -42,12 +43,10 @@ export const creationFactory = async ( 'creationFactory... after Contribution.findOneOrFail pendingCreation=', pendingCreation, ) - if (creation.confirmed) { logger.trace('creationFactory... creation.confirmed=', creation.confirmed) await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } }) logger.trace('creationFactory... after confirmContribution') - const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id }) logger.trace( 'creationFactory... after Contribution.findOneOrFail confirmedCreation=', @@ -61,6 +60,7 @@ export const creationFactory = async ( order: { balanceDate: 'DESC' }, }) logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction) + if (transaction.decay.equals(0) && transaction.creationDate) { confirmedCreation.contributionDate = new Date( nMonthsBefore(transaction.creationDate, creation.moveCreationDate), diff --git a/backend/src/typeorm/repository/User.ts b/backend/src/typeorm/repository/User.ts index b347fae40..8b3e29859 100644 --- a/backend/src/typeorm/repository/User.ts +++ b/backend/src/typeorm/repository/User.ts @@ -1,4 +1,5 @@ -import { Brackets, EntityRepository, ObjectLiteral, Repository } from '@dbTools/typeorm' +import SearchUsersFilters from '@/graphql/arg/SearchUsersFilters' +import { Brackets, EntityRepository, IsNull, Not, Repository } from '@dbTools/typeorm' import { User as DbUser } from '@entity/User' @EntityRepository(DbUser) @@ -21,17 +22,18 @@ export class UserRepository extends Repository { async findBySearchCriteriaPagedFiltered( select: string[], searchCriteria: string, - filterCriteria: ObjectLiteral[], + filters: SearchUsersFilters, currentPage: number, pageSize: number, ): Promise<[DbUser[], number]> { const query = this.createQueryBuilder('user') .select(select) + .leftJoinAndSelect('user.emailContact', 'emailContact') .withDeleted() .where( new Brackets((qb) => { qb.where( - 'user.firstName like :name or user.lastName like :lastName or user.email like :email', + 'user.firstName like :name or user.lastName like :lastName or emailContact.email like :email', { name: `%${searchCriteria}%`, lastName: `%${searchCriteria}%`, @@ -40,9 +42,23 @@ export class UserRepository extends Repository { ) }), ) + /* filterCriteria.forEach((filter) => { query.andWhere(filter) }) + */ + if (filters) { + if (filters.byActivated !== null) { + query.andWhere('emailContact.emailChecked = :value', { value: filters.byActivated }) + // filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated }) + } + + if (filters.byDeleted !== null) { + // filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() }) + query.andWhere({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() }) + } + } + return query .take(pageSize) .skip((currentPage - 1) * pageSize) From a0fe5f79519af1cfff7c9b82bf0926dba43b00f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 1 Sep 2022 01:07:25 +0200 Subject: [PATCH 36/92] adapt UserResolverTest to work with Users and UserContacts --- .../src/graphql/resolver/UserResolver.test.ts | 68 ++++++++++--------- backend/src/graphql/resolver/UserResolver.ts | 6 +- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 51f6ce073..14edc7343 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -22,6 +22,9 @@ import { ContributionLink } from '@model/ContributionLink' import { logger } from '@test/testSetup' import { validate as validateUUID, version as versionUUID } from 'uuid' import { peterLustig } from '@/seeds/users/peter-lustig' +import { UserContact } from '@entity/UserContact' +import { OptInType } from '../enum/OptInType' +import { UserContactType } from '../enum/UserContactType' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -82,7 +85,7 @@ describe('UserResolver', () => { } let result: any - let emailOptIn: string + let emailVerificationCode: string let user: User[] beforeAll(async () => { @@ -101,11 +104,11 @@ describe('UserResolver', () => { }) describe('valid input data', () => { - let loginEmailOptIn: LoginEmailOptIn[] + // let loginEmailOptIn: LoginEmailOptIn[] beforeAll(async () => { - user = await User.find() - loginEmailOptIn = await LoginEmailOptIn.find() - emailOptIn = loginEmailOptIn[0].verificationCode.toString() + user = await User.find({ relations: ['emailContact'] }) + // loginEmailOptIn = await LoginEmailOptIn.find() + emailVerificationCode = user[0].emailContact.emailVerificationCode.toString() }) describe('filling all tables', () => { @@ -115,8 +118,8 @@ describe('UserResolver', () => { id: expect.any(Number), gradidoID: expect.any(String), alias: null, - email: 'peter@lustig.de', - emailId: null, + emailContact: expect.any(UserContact), // 'peter@lustig.de', + emailId: expect.any(Number), firstName: 'Peter', lastName: 'Lustig', password: '0', @@ -124,7 +127,7 @@ describe('UserResolver', () => { privKey: null, // emailHash: expect.any(Buffer), createdAt: expect.any(Date), - emailChecked: false, + // emailChecked: false, passphrase: expect.any(String), language: 'de', isAdmin: null, @@ -141,17 +144,20 @@ describe('UserResolver', () => { }) it('creates an email optin', () => { - expect(loginEmailOptIn).toEqual([ - { - id: expect.any(Number), - userId: user[0].id, - verificationCode: expect.any(String), - emailOptInTypeId: 1, - createdAt: expect.any(Date), - resendCount: 0, - updatedAt: expect.any(Date), - }, - ]) + expect(user[0].emailContact).toEqual({ + id: expect.any(Number), + type: UserContactType.USER_CONTACT_EMAIL, + userId: user[0].id, + email: 'peter@lustig.de', + emailChecked: false, + emailVerificationCode: expect.any(String), + emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER, + emailResendCount: 0, + phone: null, + createdAt: expect.any(Date), + deletedAt: null, + updatedAt: null, + }) }) }) }) @@ -160,7 +166,7 @@ describe('UserResolver', () => { it('sends an account activation email', () => { const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( /{optin}/g, - emailOptIn, + emailVerificationCode, ).replace(/{code}/g, '') expect(sendAccountActivationEmail).toBeCalledWith({ link: activationLink, @@ -244,7 +250,7 @@ describe('UserResolver', () => { // activate account of admin Peter Lustig await mutate({ mutation: setPassword, - variables: { code: emailOptIn, password: 'Aa12345_' }, + variables: { code: emailVerificationCode, password: 'Aa12345_' }, }) // make Peter Lustig Admin const peter = await User.findOneOrFail({ id: user[0].id }) @@ -266,7 +272,9 @@ describe('UserResolver', () => { }) it('sets the contribution link id', async () => { - await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual( + await expect( + UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }), + ).resolves.toEqual( expect.objectContaining({ contributionLinkId: link.id, }), @@ -616,13 +624,13 @@ bei Gradidio sei dabei!`, describe('user exists in DB', () => { let result: any - let loginEmailOptIn: LoginEmailOptIn[] + let emailContact: UserContact beforeAll(async () => { await userFactory(testEnv, bibiBloxberg) - await resetEntity(LoginEmailOptIn) + // await resetEntity(LoginEmailOptIn) result = await mutate({ mutation: forgotPassword, variables }) - loginEmailOptIn = await LoginEmailOptIn.find() + emailContact = await UserContact.findOneOrFail(variables) }) afterAll(async () => { @@ -630,18 +638,12 @@ bei Gradidio sei dabei!`, }) it('returns true', async () => { - await expect(result).toEqual( - expect.objectContaining({ - data: { - forgotPassword: true, - }, - }), - ) + expect(result).toEqual(expect.objectContaining({ data: { forgotPassword: true } })) }) it('sends reset password email', () => { expect(sendResetPasswordEmail).toBeCalledWith({ - link: activationLink(loginEmailOptIn[0]), + link: activationLink(emailContact.emailVerificationCode), firstName: 'Bibi', lastName: 'Bloxberg', email: 'bibi@bloxberg.de', diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 514c52973..0cde3c73a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -896,17 +896,19 @@ export class UserResolver { async function findUserByEmail(email: string): Promise { const dbUserContact = await DbUserContact.findOneOrFail( { email: email }, - { withDeleted: true }, + { withDeleted: true, relations: ['user'] }, ).catch(() => { logger.error(`UserContact with email=${email} does not exists`) throw new Error('No user with this credentials') }) - const userId = dbUserContact.userId + const dbUser = dbUserContact.user + /* const dbUser = await DbUser.findOneOrFail({ id: userId }, { withDeleted: true }).catch(() => { logger.error(`User with emailContact=${email} connected per userId=${userId} does not exist`) throw new Error('No user with this credentials') }) dbUser.emailContact = dbUserContact + */ return dbUser } From 97dee4d67cf7318f957037b8bf5fdf1205861434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 2 Sep 2022 02:34:45 +0200 Subject: [PATCH 37/92] adapt tests to work with User and UserContact --- .../src/graphql/resolver/UserResolver.test.ts | 38 ++++++++------- backend/src/graphql/resolver/UserResolver.ts | 48 +++++-------------- backend/src/seeds/factory/contributionLink.ts | 6 ++- backend/src/seeds/factory/user.ts | 4 +- 4 files changed, 40 insertions(+), 56 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 14edc7343..b8d2a1d37 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -215,10 +215,12 @@ describe('UserResolver', () => { mutation: createUser, variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' }, }) - await expect(User.find()).resolves.toEqual( + await expect(User.find({ relations: ['emailContact'] }, )).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - email: 'bibi@bloxberg.de', + emailContact: expect.objectContaining({ + email: 'bibi@bloxberg.de', + }), language: 'de', }), ]), @@ -232,10 +234,12 @@ describe('UserResolver', () => { mutation: createUser, variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined }, }) - await expect(User.find()).resolves.toEqual( + await expect(User.find({ relations: ['emailContact'] }, )).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - email: 'raeuber@hotzenplotz.de', + emailContact: expect.objectContaining({ + email: 'raeuber@hotzenplotz.de', + }), publisherId: null, }), ]), @@ -276,7 +280,9 @@ describe('UserResolver', () => { UserContact.findOne({ email: 'ein@besucher.de' }, { relations: ['user'] }), ).resolves.toEqual( expect.objectContaining({ - contributionLinkId: link.id, + user: expect.objectContaining({ + contributionLinkId: link.id, + }), }), ) }) @@ -322,20 +328,20 @@ bei Gradidio sei dabei!`, } let result: any - let emailOptIn: string + let emailVerificationCode: string describe('valid optin code and valid password', () => { - let newUser: any + let newUser: User beforeAll(async () => { await mutate({ mutation: createUser, variables: createUserVariables }) - const loginEmailOptIn = await LoginEmailOptIn.find() - emailOptIn = loginEmailOptIn[0].verificationCode.toString() + const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email }) + emailVerificationCode = emailContact.emailVerificationCode.toString() result = await mutate({ mutation: setPassword, - variables: { code: emailOptIn, password: 'Aa12345_' }, + variables: { code: emailVerificationCode, password: 'Aa12345_' }, }) - newUser = await User.find() + newUser = await User.findOneOrFail({ id: emailContact.userId }, { relations: ['emailContact'] }) }) afterAll(async () => { @@ -343,11 +349,11 @@ bei Gradidio sei dabei!`, }) it('sets email checked to true', () => { - expect(newUser[0].emailChecked).toBeTruthy() + expect(newUser.emailContact.emailChecked).toBeTruthy() }) it('updates the password', () => { - expect(newUser[0].password).toEqual('3917921995996627700') + expect(newUser.password).toEqual('3917921995996627700') }) /* @@ -369,11 +375,11 @@ bei Gradidio sei dabei!`, describe('no valid password', () => { beforeAll(async () => { await mutate({ mutation: createUser, variables: createUserVariables }) - const loginEmailOptIn = await LoginEmailOptIn.find() - emailOptIn = loginEmailOptIn[0].verificationCode.toString() + const emailContact = await UserContact.findOneOrFail({ email: createUserVariables.email }) + emailVerificationCode = emailContact.emailVerificationCode.toString() result = await mutate({ mutation: setPassword, - variables: { code: emailOptIn, password: 'not-valid' }, + variables: { code: emailVerificationCode, password: 'not-valid' }, }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 0cde3c73a..64a21cef1 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -324,25 +324,6 @@ export class UserResolver { logger.info(`login with ${email}, ***, ${publisherId} ...`) email = email.trim().toLowerCase() const dbUser = await findUserByEmail(email) - /* - const dbUserContact = await DbUserContact.findOneOrFail({ email }, { withDeleted: true }).catch( - () => { - logger.error(`UserContact with email=${email} does not exists`) - throw new Error('No user with this credentials') - }, - ) - const userId = dbUserContact.userId - const dbUser = await DbUser.findOneOrFail(userId).catch(() => { - logger.error(`User with emeilContact=${email} connected per userId=${userId} does not exist`) - throw new Error('No user with this credentials') - }) - */ - /* - const dbUser = await DbUser.findOneOrFail({ email }, { withDeleted: true }).catch(() => { - logger.error(`User with email=${email} does not exists`) - throw new Error('No user with this credentials') - }) - */ if (dbUser.deletedAt) { logger.error('The User was permanently deleted in database.') throw new Error('This user was permanently deleted. Contact support for questions.') @@ -591,8 +572,9 @@ export class UserResolver { async forgotPassword(@Arg('email') email: string): Promise { logger.info(`forgotPassword(${email})...`) email = email.trim().toLowerCase() - const user = await findUserByEmail(email) - // const user = await DbUser.findOne({ email }) + const user = await findUserByEmail(email).catch(() => { + logger.warn(`fail on find UserContact per ${email}`) + }) if (!user) { logger.warn(`no user found with ${email}`) return true @@ -650,12 +632,13 @@ export class UserResolver { throw new Error('Could not login with emailVerificationCode') }) */ - const userContact = await DbUserContact.findOneOrFail({ emailVerificationCode: code }).catch( - () => { - logger.error('Could not login with emailVerificationCode') - throw new Error('Could not login with emailVerificationCode') - }, - ) + const userContact = await DbUserContact.findOneOrFail( + { emailVerificationCode: code }, + { relations: ['user'] }, + ).catch(() => { + logger.error('Could not login with emailVerificationCode') + throw new Error('Could not login with emailVerificationCode') + }) logger.debug('userContact loaded...') // Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes if (!isEmailVerificationCodeValid(userContact.updatedAt)) { @@ -669,10 +652,7 @@ export class UserResolver { logger.debug('EmailVerificationCode is valid...') // load user - const user = await DbUser.findOneOrFail({ id: userContact.userId }).catch(() => { - logger.error('Could not find corresponding Login User') - throw new Error('Could not find corresponding Login User') - }) + const user = userContact.user logger.debug('user with EmailVerificationCode found...') // Generate Passphrase if needed @@ -902,13 +882,7 @@ async function findUserByEmail(email: string): Promise { throw new Error('No user with this credentials') }) const dbUser = dbUserContact.user - /* - const dbUser = await DbUser.findOneOrFail({ id: userId }, { withDeleted: true }).catch(() => { - logger.error(`User with emailContact=${email} connected per userId=${userId} does not exist`) - throw new Error('No user with this credentials') - }) dbUser.emailContact = dbUserContact - */ return dbUser } diff --git a/backend/src/seeds/factory/contributionLink.ts b/backend/src/seeds/factory/contributionLink.ts index 5c83b6ad3..b422993e1 100644 --- a/backend/src/seeds/factory/contributionLink.ts +++ b/backend/src/seeds/factory/contributionLink.ts @@ -3,6 +3,7 @@ import { createContributionLink } from '@/seeds/graphql/mutations' import { login } from '@/seeds/graphql/queries' import { ContributionLink } from '@model/ContributionLink' import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface' +import { User } from '@/graphql/model/User' export const contributionLinkFactory = async ( client: ApolloServerTestClient, @@ -11,7 +12,10 @@ export const contributionLinkFactory = async ( const { mutate, query } = client // login as admin - await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } }) + const user = await query({ + query: login, + variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, + }) const variables = { amount: contributionLink.amount, diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index c2eb20bc4..df6e1ef6b 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -17,10 +17,10 @@ export const userFactory = async ( } = await mutate({ mutation: createUser, variables: user }) // console.log('creatUser:', { id }, { user }) // get user from database - let dbUser = await User.findOneOrFail({ id }) + let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact']}) // console.log('dbUser:', dbUser) - const emailContact = await UserContact.findOneOrFail({ userId: id }) + const emailContact = dbUser.emailContact // console.log('emailContact:', emailContact) if (user.emailChecked) { From 0619fb2f6714c3a8fb894dfbe531420e3d6d98e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 6 Sep 2022 00:26:44 +0200 Subject: [PATCH 38/92] tests for Password handling --- .../src/graphql/resolver/UserResolver.test.ts | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index b8d2a1d37..96ef634ce 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -7,7 +7,6 @@ import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' -import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' @@ -215,7 +214,7 @@ describe('UserResolver', () => { mutation: createUser, variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' }, }) - await expect(User.find({ relations: ['emailContact'] }, )).resolves.toEqual( + await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ emailContact: expect.objectContaining({ @@ -234,7 +233,7 @@ describe('UserResolver', () => { mutation: createUser, variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined }, }) - await expect(User.find({ relations: ['emailContact'] }, )).resolves.toEqual( + await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ emailContact: expect.objectContaining({ @@ -281,7 +280,7 @@ describe('UserResolver', () => { ).resolves.toEqual( expect.objectContaining({ user: expect.objectContaining({ - contributionLinkId: link.id, + contributionLinkId: link.id, }), }), ) @@ -341,7 +340,10 @@ bei Gradidio sei dabei!`, mutation: setPassword, variables: { code: emailVerificationCode, password: 'Aa12345_' }, }) - newUser = await User.findOneOrFail({ id: emailContact.userId }, { relations: ['emailContact'] }) + newUser = await User.findOneOrFail( + { id: emailContact.userId }, + { relations: ['emailContact'] }, + ) }) afterAll(async () => { @@ -616,35 +618,63 @@ bei Gradidio sei dabei!`, describe('forgotPassword', () => { const variables = { email: 'bibi@bloxberg.de' } + const emailCodeRequestTime = CONFIG.EMAIL_CODE_REQUEST_TIME + describe('user is not in DB', () => { - it('returns true', async () => { - await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( - expect.objectContaining({ - data: { - forgotPassword: true, - }, - }), - ) + describe('duration not expired', () => { + it('returns true', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) }) }) describe('user exists in DB', () => { - let result: any let emailContact: UserContact beforeAll(async () => { await userFactory(testEnv, bibiBloxberg) // await resetEntity(LoginEmailOptIn) - result = await mutate({ mutation: forgotPassword, variables }) emailContact = await UserContact.findOneOrFail(variables) }) afterAll(async () => { await cleanDB() + CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime }) - it('returns true', async () => { - expect(result).toEqual(expect.objectContaining({ data: { forgotPassword: true } })) + describe('duration not expired', () => { + it('returns true', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + `email already sent less than ${printTimeDuration( + CONFIG.EMAIL_CODE_REQUEST_TIME, + )} minutes ago`, + ), + ], + }), + ) + }) + }) + + describe('duration reset to 0', () => { + it('returns true', async () => { + CONFIG.EMAIL_CODE_REQUEST_TIME = 0 + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) }) it('sends reset password email', () => { @@ -659,6 +689,7 @@ bei Gradidio sei dabei!`, describe('request reset password again', () => { it('thows an error', async () => { + CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')], @@ -670,11 +701,13 @@ bei Gradidio sei dabei!`, }) describe('queryOptIn', () => { - let loginEmailOptIn: LoginEmailOptIn[] + // let loginEmailOptIn: LoginEmailOptIn[] + let emailContact: UserContact beforeAll(async () => { await userFactory(testEnv, bibiBloxberg) - loginEmailOptIn = await LoginEmailOptIn.find() + // loginEmailOptIn = await LoginEmailOptIn.find() + emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email }) }) afterAll(async () => { @@ -689,8 +722,8 @@ bei Gradidio sei dabei!`, expect.objectContaining({ errors: [ // keep Whitspace in error message! - new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: { - "verificationCode": "not-valid" + new GraphQLError(`Could not find any entity of type "UserContact" matching: { + "emailVerificationCode": "not-valid" }`), ], }), @@ -703,7 +736,7 @@ bei Gradidio sei dabei!`, await expect( query({ query: queryOptIn, - variables: { optIn: loginEmailOptIn[0].verificationCode.toString() }, + variables: { optIn: emailContact.emailVerificationCode.toString() }, }), ).resolves.toEqual( expect.objectContaining({ From e6155d52e1906ac8fdb38aa24dc132c25b1ffccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 12 Sep 2022 18:34:21 +0200 Subject: [PATCH 39/92] solve error --- backend/src/graphql/resolver/AdminResolver.ts | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 167390a21..21627b099 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -417,30 +417,16 @@ export class AdminResolver { relations: ['emailContact'], }) + return contributions.map((contribution) => { + const user = users.find((u) => u.id === contribution.userId) + const creation = userCreations.find((c) => c.id === contribution.userId) + return new UnconfirmedContribution( contribution, user, creation ? creation.creations : FULL_CREATION_AVAILABLE, ) - /* - return contributions.map((contribution) => { - const user = users.find((u) => u.id === contribution.userId) - const creation = userCreations.find((c) => c.id === contribution.userId) - - return { - id: contribution.id, - userId: contribution.userId, - date: contribution.contributionDate, - memo: contribution.memo, - amount: contribution.amount, - moderator: contribution.moderatorId, - firstName: user ? user.firstName : '', - lastName: user ? user.lastName : '', - email: user ? user.emailContact.email : '', - creation: creation ? creation.creations : FULL_CREATION_AVAILABLE, - } }) - */ } @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) From 68ba6def1409b10df904bcce142aa4acb21e8b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 12 Sep 2022 18:41:17 +0200 Subject: [PATCH 40/92] now find users including emailContact --- backend/src/util/klicktipp.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/util/klicktipp.ts b/backend/src/util/klicktipp.ts index c8f83acc3..0432f196e 100644 --- a/backend/src/util/klicktipp.ts +++ b/backend/src/util/klicktipp.ts @@ -7,16 +7,16 @@ export async function retrieveNotRegisteredEmails(): Promise { if (!con) { throw new Error('No connection to database') } - const users = await User.find() + const users = await User.find({ relations: ['emailContact'] }) const notRegisteredUser = [] for (let i = 0; i < users.length; i++) { const user = users[i] try { - await getKlickTippUser(user.email) + await getKlickTippUser(user.emailContact.email) } catch (err) { - notRegisteredUser.push(user.email) + notRegisteredUser.push(user.emailContact.email) // eslint-disable-next-line no-console - console.log(`${user.email}`) + console.log(`${user.emailContact.email}`) } } await con.close() From 298924001cb26919735db2e3a02962138fb519b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 12 Sep 2022 18:45:42 +0200 Subject: [PATCH 41/92] linting --- database/migrations/0049-add_user_contacts_table.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/database/migrations/0049-add_user_contacts_table.ts b/database/migrations/0049-add_user_contacts_table.ts index 82c2555ab..c3b89ed88 100644 --- a/database/migrations/0049-add_user_contacts_table.ts +++ b/database/migrations/0049-add_user_contacts_table.ts @@ -7,10 +7,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { v4 as uuidv4 } from 'uuid' - export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { - await queryFn(` CREATE TABLE IF NOT EXISTS \`user_contacts\` ( \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT, From 7cd5ecb463ad5e32917d3a94ddbfb99889829942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 12 Sep 2022 19:02:12 +0200 Subject: [PATCH 42/92] linting --- backend/src/graphql/resolver/AdminResolver.ts | 2 -- backend/src/graphql/resolver/UserResolver.test.ts | 2 +- backend/src/seeds/factory/contributionLink.ts | 2 +- backend/src/seeds/factory/creation.ts | 1 - backend/src/seeds/factory/user.ts | 3 +-- backend/src/webhook/elopage.ts | 1 - database/entity/0049-add_user_contacts_table/UserContact.ts | 1 - 7 files changed, 3 insertions(+), 9 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 21627b099..021978710 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -4,8 +4,6 @@ import { Resolver, Query, Arg, Args, Authorized, Mutation, Ctx, Int } from 'type import { getCustomRepository, IsNull, - Not, - ObjectLiteral, getConnection, In, MoreThan, diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 96ef634ce..5db5e3fc4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers' +import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' diff --git a/backend/src/seeds/factory/contributionLink.ts b/backend/src/seeds/factory/contributionLink.ts index b422993e1..d8f31d585 100644 --- a/backend/src/seeds/factory/contributionLink.ts +++ b/backend/src/seeds/factory/contributionLink.ts @@ -3,7 +3,6 @@ import { createContributionLink } from '@/seeds/graphql/mutations' import { login } from '@/seeds/graphql/queries' import { ContributionLink } from '@model/ContributionLink' import { ContributionLinkInterface } from '@/seeds/contributionLink/ContributionLinkInterface' -import { User } from '@/graphql/model/User' export const contributionLinkFactory = async ( client: ApolloServerTestClient, @@ -12,6 +11,7 @@ export const contributionLinkFactory = async ( const { mutate, query } = client // login as admin + // eslint-disable-next-line @typescript-eslint/no-unused-vars const user = await query({ query: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index 7f19e2828..99fd39d3b 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -6,7 +6,6 @@ import { adminCreateContribution, confirmContribution } from '@/seeds/graphql/mu import { login } from '@/seeds/graphql/queries' import { CreationInterface } from '@/seeds/creation/CreationInterface' import { ApolloServerTestClient } from 'apollo-server-testing' -import { User } from '@entity/User' import { Transaction } from '@entity/Transaction' import { Contribution } from '@entity/Contribution' import { UserContact } from '@entity/UserContact' diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index df6e1ef6b..faa34e31a 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -2,7 +2,6 @@ import { createUser, setPassword } from '@/seeds/graphql/mutations' import { User } from '@entity/User' import { UserInterface } from '@/seeds/users/UserInterface' import { ApolloServerTestClient } from 'apollo-server-testing' -import { UserContact } from '@entity/UserContact' export const userFactory = async ( client: ApolloServerTestClient, @@ -17,7 +16,7 @@ export const userFactory = async ( } = await mutate({ mutation: createUser, variables: user }) // console.log('creatUser:', { id }, { user }) // get user from database - let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact']}) + let dbUser = await User.findOneOrFail({ id }, { relations: ['emailContact'] }) // console.log('dbUser:', dbUser) const emailContact = dbUser.emailContact diff --git a/backend/src/webhook/elopage.ts b/backend/src/webhook/elopage.ts index 6c8ca7e49..87af4088c 100644 --- a/backend/src/webhook/elopage.ts +++ b/backend/src/webhook/elopage.ts @@ -29,7 +29,6 @@ import { LoginElopageBuys } from '@entity/LoginElopageBuys' import { UserResolver } from '@/graphql/resolver/UserResolver' -import { User as dbUser } from '@entity/User' import { UserContact as dbUserContact } from '@entity/UserContact' export const elopageWebhook = async (req: any, res: any): Promise => { diff --git a/database/entity/0049-add_user_contacts_table/UserContact.ts b/database/entity/0049-add_user_contacts_table/UserContact.ts index 20732ae6f..97b12d4cd 100644 --- a/database/entity/0049-add_user_contacts_table/UserContact.ts +++ b/database/entity/0049-add_user_contacts_table/UserContact.ts @@ -5,7 +5,6 @@ import { Column, DeleteDateColumn, OneToOne, - JoinColumn, } from 'typeorm' import { User } from './User' From d9313b68dbda117d2187fb7b750a8f2b6abbbefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 12 Sep 2022 19:26:59 +0200 Subject: [PATCH 43/92] init moderator in UnconfirmedContribution --- backend/src/graphql/model/UnconfirmedContribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/model/UnconfirmedContribution.ts b/backend/src/graphql/model/UnconfirmedContribution.ts index bea53fdec..c42b4fd11 100644 --- a/backend/src/graphql/model/UnconfirmedContribution.ts +++ b/backend/src/graphql/model/UnconfirmedContribution.ts @@ -14,7 +14,7 @@ export class UnconfirmedContribution { this.firstName = user ? user.firstName : '' this.lastName = user ? user.lastName : '' this.email = user ? user.emailContact.email : '' - // this.moderator = contribution.moderatorId + this.moderator = contribution.moderatorId this.creation = creations this.state = contribution.contributionStatus this.messageCount = contribution.messages ? contribution.messages.length : 0 From 13d79fd8b7a714e56bee426441f94cc819de6d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Wed, 14 Sep 2022 23:00:47 +0200 Subject: [PATCH 44/92] remove multi-line comments --- backend/src/graphql/resolver/AdminResolver.ts | 45 ------------------- 1 file changed, 45 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 021978710..b33f65404 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -77,18 +77,6 @@ export class AdminResolver { { searchText, currentPage = 1, pageSize = 25, filters }: SearchUsersArgs, ): Promise { const userRepository = getCustomRepository(UserRepository) - /* - const filterCriteria: ObjectLiteral[] = [] - if (filters) { - if (filters.byActivated !== null) { - filterCriteria.push({ 'emailContact.emailChecked': filters.byActivated }) - } - - if (filters.byDeleted !== null) { - filterCriteria.push({ deletedAt: filters.byDeleted ? Not(IsNull()) : IsNull() }) - } - } - */ const userFields = [ 'id', 'firstName', @@ -121,27 +109,6 @@ export class AdminResolver { users.map(async (user) => { let emailConfirmationSend = '' if (!user.emailContact.emailChecked) { - /* - const emailOptIn = await LoginEmailOptIn.findOne( - { - userId: user.id, - }, - { - order: { - updatedAt: 'DESC', - createdAt: 'DESC', - }, - select: ['updatedAt', 'createdAt'], - }, - ) - if (emailOptIn) { - if (emailOptIn.updatedAt) { - emailConfirmationSend = emailOptIn.updatedAt.toISOString() - } else { - emailConfirmationSend = emailOptIn.createdAt.toISOString() - } - } - */ if (user.emailContact.updatedAt) { emailConfirmationSend = user.emailContact.updatedAt.toISOString() } else { @@ -558,18 +525,6 @@ export class AdminResolver { throw new Error(`Could not find User to emailContact: ${email}`) } - /* - const user = await dbUser.findOneOrFail({ email: email }) - - // can be both types: REGISTER and RESET_PASSWORD - let optInCode = await LoginEmailOptIn.findOne({ - where: { userId: user.id }, - order: { updatedAt: 'DESC' }, - }) - - optInCode = await checkOptInCode(optInCode, user) - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ link: activationLink(emailContact.emailVerificationCode), From 64aab998e2840327443527e5efd54f36bebc0b49 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Wed, 14 Sep 2022 23:57:53 +0200 Subject: [PATCH 45/92] Update backend/src/graphql/resolver/UserResolver.test.ts Co-authored-by: Moriz Wahl --- backend/src/graphql/resolver/UserResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 5db5e3fc4..5fef81ef1 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -142,7 +142,7 @@ describe('UserResolver', () => { expect(verUUID).toEqual(4) }) - it('creates an email optin', () => { + it('creates an email contact', () => { expect(user[0].emailContact).toEqual({ id: expect.any(Number), type: UserContactType.USER_CONTACT_EMAIL, From 45330a60fadd24ea9babae3996d5a0a5110bc34c Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Thu, 15 Sep 2022 00:59:45 +0200 Subject: [PATCH 46/92] Update database/entity/0049-add_user_contacts_table/User.ts Co-authored-by: Moriz Wahl --- database/entity/0049-add_user_contacts_table/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/entity/0049-add_user_contacts_table/User.ts b/database/entity/0049-add_user_contacts_table/User.ts index e3ac7d591..bf8ca3277 100644 --- a/database/entity/0049-add_user_contacts_table/User.ts +++ b/database/entity/0049-add_user_contacts_table/User.ts @@ -120,7 +120,7 @@ export class User extends BaseEntity { @JoinColumn({ name: 'user_id' }) messages?: ContributionMessage[] - @OneToMany(() => UserContact, (usercontact: UserContact) => usercontact.user) + @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) @JoinColumn({ name: 'user_id' }) usercontacts?: UserContact[] } From 9a99a8741d0f28672982cf7c090d7e4e6fc00586 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Thu, 15 Sep 2022 01:05:34 +0200 Subject: [PATCH 47/92] Update database/entity/0049-add_user_contacts_table/User.ts Co-authored-by: Moriz Wahl --- database/entity/0049-add_user_contacts_table/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/entity/0049-add_user_contacts_table/User.ts b/database/entity/0049-add_user_contacts_table/User.ts index bf8ca3277..abe40df54 100644 --- a/database/entity/0049-add_user_contacts_table/User.ts +++ b/database/entity/0049-add_user_contacts_table/User.ts @@ -122,5 +122,5 @@ export class User extends BaseEntity { @OneToMany(() => UserContact, (userContact: UserContact) => userContact.user) @JoinColumn({ name: 'user_id' }) - usercontacts?: UserContact[] + userContacts?: UserContact[] } From 411e03c843a146ecc4f55cd741fedcdc9bc6a60d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 15 Sep 2022 02:40:59 +0200 Subject: [PATCH 48/92] rework PR comments --- backend/src/graphql/resolver/AdminResolver.ts | 20 +- .../resolver/TransactionLinkResolver.ts | 5 +- .../graphql/resolver/TransactionResolver.ts | 8 +- backend/src/graphql/resolver/UserResolver.ts | 2 +- backend/src/seeds/factory/user.ts | 2 +- .../UC_Introduction_of_Gradido-ID.md | 318 +++++++++--------- 6 files changed, 184 insertions(+), 171 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index b33f65404..488a39a22 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -41,7 +41,7 @@ import Paginated from '@arg/Paginated' import TransactionLinkFilters from '@arg/TransactionLinkFilters' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' -import { activationLink, printTimeDuration } from './UserResolver' +import { findUserByEmail, activationLink, printTimeDuration } from './UserResolver' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver' import CONFIG from '@/config' @@ -403,6 +403,7 @@ export class AdminResolver { throw new Error('Contribution not found for given id.') } contribution.contributionStatus = ContributionStatus.DELETED + await contribution.save() const res = await contribution.softRemove() return !!res } @@ -514,16 +515,21 @@ export class AdminResolver { @Mutation(() => Boolean) async sendActivationEmail(@Arg('email') email: string): Promise { email = email.trim().toLowerCase() - const emailContact = await UserContact.findOne({ email: email }) - if (!emailContact) { - logger.error(`Could not find UserContact with email: ${email}`) - throw new Error(`Could not find UserContact with email: ${email}`) - } - const user = await dbUser.findOne({ id: emailContact.userId }) + // const user = await dbUser.findOne({ id: emailContact.userId }) + const user = await findUserByEmail(email) if (!user) { logger.error(`Could not find User to emailContact: ${email}`) throw new Error(`Could not find User to emailContact: ${email}`) } + if (user.deletedAt) { + logger.error(`User with emailContact: ${email} is deleted.`) + throw new Error(`User with emailContact: ${email} is deleted.`) + } + const emailContact = user.emailContact + if (emailContact.deletedAt) { + logger.error(`The emailContact: ${email} of htis User is deleted.`) + throw new Error(`The emailContact: ${email} of htis User is deleted.`) + } // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendAccountActivationEmail({ diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index ccc0f628d..194126f3f 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -283,7 +283,10 @@ export class TransactionLinkResolver { return true } else { const transactionLink = await dbTransactionLink.findOneOrFail({ code }) - const linkedUser = await dbUser.findOneOrFail({ id: transactionLink.userId }) + const linkedUser = await dbUser.findOneOrFail( + { id: transactionLink.userId }, + { relations: ['user'] }, + ) if (user.id === linkedUser.id) { throw new Error('Cannot redeem own transaction link.') diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index ae6445343..3cd871fc3 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -36,6 +36,7 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { UserContact } from '@entity/UserContact' +import { findUserByEmail } from './UserResolver' export const executeTransaction = async ( amount: Decimal, @@ -294,13 +295,15 @@ export class TransactionResolver { } // validate recipient user + const recipientUser = await findUserByEmail(email) + /* const emailContact = await UserContact.findOne({ email }, { withDeleted: true }) if (!emailContact) { logger.error(`Could not find UserContact with email: ${email}`) throw new Error(`Could not find UserContact with email: ${email}`) } - - const recipientUser = await dbUser.findOne({ id: emailContact.userId }) + */ + // const recipientUser = await dbUser.findOne({ id: emailContact.userId }) if (!recipientUser) { logger.error(`unknown recipient to UserContact: email=${email}`) throw new Error('unknown recipient') @@ -309,6 +312,7 @@ export class TransactionResolver { logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`) throw new Error('The recipient account was deleted') } + const emailContact = recipientUser.emailContact if (!emailContact.emailChecked) { logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`) throw new Error('The recipient account is not activated') diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 64a21cef1..19829c95f 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -873,7 +873,7 @@ export class UserResolver { } } -async function findUserByEmail(email: string): Promise { +export async function findUserByEmail(email: string): Promise { const dbUserContact = await DbUserContact.findOneOrFail( { email: email }, { withDeleted: true, relations: ['user'] }, diff --git a/backend/src/seeds/factory/user.ts b/backend/src/seeds/factory/user.ts index faa34e31a..d566275db 100644 --- a/backend/src/seeds/factory/user.ts +++ b/backend/src/seeds/factory/user.ts @@ -40,7 +40,7 @@ export const userFactory = async ( } // get last changes of user from database - dbUser = await User.findOneOrFail({ id }, { withDeleted: true }) + // dbUser = await User.findOneOrFail({ id }, { withDeleted: true }) return dbUser } diff --git a/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md b/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md index c8eb12524..9d607ba97 100644 --- a/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md +++ b/docu/Concepts/TechnicalRequirements/UC_Introduction_of_Gradido-ID.md @@ -1,159 +1,159 @@ -# Introduction of Gradido-ID - -## Motivation - -The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage. - -Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account. - -## Definition - -The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID). - -## Steps of Introduction - -To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all. - -The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID. - -### Database-Schema - -#### Users-Table - -The entity users has to be changed by adding the following columns. - -| Column | Type | Description | -| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- | -| gradidoID | String | technical unique key of the user as UUID (version 4) | -| alias | String | a business unique key of the user | -| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... | -| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts | - -##### Email vs emailID - -The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table. - -The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table. - -#### new UserContacts-Table - -A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses. - -| Column | Type | Description | -| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | int | the technical key of a contact entity | -| type | int | Defines the type of contact entry as enum: Email, Phone, etc | -| userID | int | Defines the foreign key to the `Users` table | -| email | String | defines the address of a contact entry of type Email | -| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset | -| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 | -| emailResendCount | int | counter how often the email was resend | -| emailChecked | boolean | flag if email is verified and confirmed | -| createdAt | DateTime | point of time the Contact was created | -| updatedAt | DateTime | point of time the Contact was updated | -| deletedAt | DateTime | point of time the Contact was soft deleted | -| phone | String | defines the address of a contact entry of type Phone | -| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... | - -### Database-Migration - -After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables. - -#### Initialize GradidoID - -In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4). - -#### Primary Email Contact - -In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with: - -* id = new technical key -* type = Enum-Email -* userID = `Users.id` -* email = `Users.email` -* emailVerifyCode = `login_email_opt_in.verification_code` -* emailOptInType = `login_email_opt_in.email_opt_in_type_id` -* emailResendCount = `login_email_opt_in.resent_count` -* emailChecked = `Users.emailChecked` -* createdAt = `login_email_opt_in.created_at` -* updatedAt = `login_email_opt_in.updated_at` -* phone = null -* usedChannel = Enum-"main contact" - -and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1` - -After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted. - -### Adaption of BusinessLogic - -The following logic or business processes has to be adapted for introducing the Gradido-ID - -#### Read-Write Access of Users-Table especially Email - -The ORM mapping has to be adapted to the changed and new database schema. - -#### Registration Process - -The logic of the registration process has to be adapted by - -* initializing the `Users.userID` with a unique UUID -* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel ` -* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry -* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email` - -#### Login Process - -The logic of the login process has to be adapted by - -* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input -* depending on the `Users.passphraseEncryptionType` decrypt the stored password - * = 1 : with the email - * = 2 : with the userID - -#### Password En/Decryption - -The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID. - -#### Change Password Process - -The logic of change password has to be adapted by - -* if the `Users.passphraseEncryptionType` = 1, then - - * read the users email address from the `UsersContact `table - * give the email address as input for the password decryption of the existing password - * use the `Users.userID` as input for the password encryption for the new password - * change the `Users.passphraseEnrycptionType` to the new value =2 -* if the `Users.passphraseEncryptionType` = 2, then - - * give the `Users.userID` as input for the password decryption of the existing password - * use the `Users.userID` as input for the password encryption fo the new password - -#### Search- and Access Logic - -A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by - -* searching per email (only with maincontact as contactchannel) -* searching per userID -* searching per alias - -#### Identity-Mapping - -A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output: - -* email -> userID -* email -> gradidoID -* email -> alias -* userID -> gradidoID -* userID -> email -* userID -> alias -* alias -> gradidoID -* alias -> email -* alias -> userID -* gradidoID -> email -* gradidoID -> userID -* gradidoID -> alias - -#### GDT-Access - -To use the GDT-servers api the used identifier for GDT has to be switch from email to userID. +# Introduction of Gradido-ID + +## Motivation + +The introduction of the Gradido-ID base on the requirement to identify an user account per technical key instead of using an email-address. Such a technical key ensures an exact identification of an user account without giving detailed information for possible missusage. + +Additionally the Gradido-ID allows to administrade any user account data like changing the email address or define several email addresses without any side effects on the identification of the user account. + +## Definition + +The formalized definition of the Gradido-ID can be found in the document [BenutzerVerwaltung#Gradido-ID](../BusinessRequirements/BenutzerVerwaltung#Gradido-ID). + +## Steps of Introduction + +To Introduce the Gradido-ID there are several steps necessary. The first step is to define a proper database schema with additional columns and tables followed by data migration steps to add or initialize the new columns and tables by keeping valid data at all. + +The second step is to decribe all concerning business logic processes, which have to be adapted by introducing the Gradido-ID. + +### Database-Schema + +#### Users-Table + +The entity users has to be changed by adding the following columns. + +| Column | Type | Description | +| ------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------- | +| gradidoID | String | technical unique key of the user as UUID (version 4) | +| alias | String | a business unique key of the user | +| passphraseEncryptionType | int | defines the type of encrypting the passphrase: 1 = email (default), 2 = gradidoID, ... | +| emailID | int | technical foreign key to the entry with type Email and contactChannel=maincontact of the new entity UserContacts | + +##### Email vs emailID + +The existing column `email`, will now be changed to the primary email contact, which will be stored as a contact entry in the new `UserContacts` table. It is necessary to decide if the content of the `email `will be changed to the foreign key `emailID `to the contact entry with the email address or if the email itself will be kept as a denormalized and duplicate value in the `users `table. + +The preferred and proper solution will be to add a new column `Users.emailId `as foreign key to the `UsersContact `entry and delete the `Users.email` column after the migration of the email address in the `UsersContact `table. + +#### new UserContacts-Table + +A new entity `UserContacts `is introduced to store several contacts of different types like email, telephone or other kinds of contact addresses. + +| Column | Type | Description | +| --------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | int | the technical key of a contact entity | +| type | int | Defines the type of contact entry as enum: Email, Phone, etc | +| userID | int | Defines the foreign key to the `Users` table | +| email | String | defines the address of a contact entry of type Email | +| emailVerificationCode | unsinged bigint(20) | unique code to verify email or password reset | +| emailOptInType | int | REGISTER=1, RESET_PASSWORD=2 | +| emailResendCount | int | counter how often the email was resend | +| emailChecked | boolean | flag if email is verified and confirmed | +| createdAt | DateTime | point of time the Contact was created | +| updatedAt | DateTime | point of time the Contact was updated | +| deletedAt | DateTime | point of time the Contact was soft deleted | +| phone | String | defines the address of a contact entry of type Phone | +| contactChannels | String | define the contact channel as comma separated list for which this entry is confirmed by the user e.g. main contact (default), infomail, contracting, advertisings, ... | + +### Database-Migration + +After the adaption of the database schema and to keep valid consistent data, there must be several steps of data migration to initialize the new and changed columns and tables. + +#### Initialize GradidoID + +In a one-time migration create for each entry of the `Users `tabel an unique UUID (version4). + +#### Primary Email Contact + +In a one-time migration read for each entry of the `Users `table the `Users.id` and `Users.email`, select from the table `login_email_opt_in` the entry with the `login_email_opt_in.user_id` = `Users.id` and create a new entry in the `UsersContact `table, by initializing the contact-values with: + +* id = new technical key +* type = Enum-Email +* userID = `Users.id` +* email = `Users.email` +* emailVerifyCode = `login_email_opt_in.verification_code` +* emailOptInType = `login_email_opt_in.email_opt_in_type_id` +* emailResendCount = `login_email_opt_in.resent_count` +* emailChecked = `Users.emailChecked` +* createdAt = `login_email_opt_in.created_at` +* updatedAt = `login_email_opt_in.updated_at` +* phone = null +* usedChannel = Enum-"main contact" + +and update the `Users `entry with `Users.emailId = UsersContact.Id` and `Users.passphraseEncryptionType = 1` + +After this one-time migration and a verification, which ensures that all data are migrated, then the columns `Users.email`, `Users.emailChecked`, `Users.emailHash` and the table `login_email_opt_in` can be deleted. + +### Adaption of BusinessLogic + +The following logic or business processes has to be adapted for introducing the Gradido-ID + +#### Read-Write Access of Users-Table especially Email + +The ORM mapping has to be adapted to the changed and new database schema. + +#### Registration Process + +The logic of the registration process has to be adapted by + +* initializing the `Users.userID` with a unique UUID +* creating a new `UsersContact `entry with the given email address and *maincontact* as `usedChannel ` +* set `emailID `in the `Users `table as foreign key to the new `UsersContact `entry +* set `Users.passphraseEncrpytionType = 2` and encrypt the passphrase with the `Users.userID` instead of the `UsersContact.email` + +#### Login Process + +The logic of the login process has to be adapted by + +* search the users data by reading the `Users `and the `UsersContact` table with the email (or alias as soon as the user can maintain his profil with an alias) as input +* depending on the `Users.passphraseEncryptionType` decrypt the stored password + * = 1 : with the email + * = 2 : with the userID + +#### Password En/Decryption + +The logic of the password en/decryption has to be adapted by encapsulate the logic to be controlled with an input parameter. The input parameter can be the email or the userID. + +#### Change Password Process + +The logic of change password has to be adapted by + +* if the `Users.passphraseEncryptionType` = 1, then + + * read the users email address from the `UsersContact `table + * give the email address as input for the password decryption of the existing password + * use the `Users.userID` as input for the password encryption for the new password + * change the `Users.passphraseEnrycptionType` to the new value =2 +* if the `Users.passphraseEncryptionType` = 2, then + + * give the `Users.userID` as input for the password decryption of the existing password + * use the `Users.userID` as input for the password encryption fo the new password + +#### Search- and Access Logic + +A new logic has to be introduced to search the user identity per different input values. That means searching the user data must be possible by + +* searching per email (only with maincontact as contactchannel) +* searching per userID +* searching per alias + +#### Identity-Mapping + +A new mapping logic will be necessary to allow using unmigrated APIs like GDT-servers api. So it must be possible to give this identity-mapping logic the following input to get the respective output: + +* email -> userID +* email -> gradidoID +* email -> alias +* userID -> gradidoID +* userID -> email +* userID -> alias +* alias -> gradidoID +* alias -> email +* alias -> userID +* gradidoID -> email +* gradidoID -> userID +* gradidoID -> alias + +#### GDT-Access + +To use the GDT-servers api the used identifier for GDT has to be switch from email to userID. From ad66d2519ee5f0b2335f4681201e447a5b8be1e8 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 15 Sep 2022 12:29:28 +0200 Subject: [PATCH 49/92] refactor: Add Client Request Time to Client Request and Add it to Context --- frontend/src/plugins/apolloProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/plugins/apolloProvider.js b/frontend/src/plugins/apolloProvider.js index 4a0ff9914..05954d36b 100644 --- a/frontend/src/plugins/apolloProvider.js +++ b/frontend/src/plugins/apolloProvider.js @@ -12,6 +12,7 @@ const authLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { Authorization: token && token.length > 0 ? `Bearer ${token}` : '', + clientRequestTime: new Date().toString(), }, }) return forward(operation).map((response) => { From 80bbf2411669740747efcc3271bc49bc434ee88c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 15 Sep 2022 12:30:29 +0200 Subject: [PATCH 50/92] add client request time to context --- backend/src/server/context.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/backend/src/server/context.ts b/backend/src/server/context.ts index d9fd55fe4..5bfc22e72 100644 --- a/backend/src/server/context.ts +++ b/backend/src/server/context.ts @@ -9,6 +9,7 @@ export interface Context { setHeaders: { key: string; value: string }[] role?: Role user?: dbUser + clientRequestTime?: string // hack to use less DB calls for Balance Resolver lastTransaction?: dbTransaction transactionCount?: number @@ -18,14 +19,17 @@ export interface Context { const context = (args: ExpressContext): Context => { const authorization = args.req.headers.authorization - let token: string | null = null - if (authorization) { - token = authorization.replace(/^Bearer /, '') - } - const context = { - token, + const clientRequestTime = args.req.headers.clientrequesttime + const context: Context = { + token: null, setHeaders: [], } + if (authorization) { + context.token = authorization.replace(/^Bearer /, '') + } + if (clientRequestTime && typeof clientRequestTime === 'string') { + context.clientRequestTime = clientRequestTime + } return context } From 23f5ebabd3d2eb5be98d5437b7e61b4629066721 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 15 Sep 2022 12:35:08 +0200 Subject: [PATCH 51/92] add client request time to request header of admin interface --- admin/src/plugins/apolloProvider.js | 1 + 1 file changed, 1 insertion(+) diff --git a/admin/src/plugins/apolloProvider.js b/admin/src/plugins/apolloProvider.js index 1f51be20a..95b7aab7e 100644 --- a/admin/src/plugins/apolloProvider.js +++ b/admin/src/plugins/apolloProvider.js @@ -10,6 +10,7 @@ const authLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { Authorization: token && token.length > 0 ? `Bearer ${token}` : '', + clientRequestTime: new Date().toString(), }, }) return forward(operation).map((response) => { From bb8fc6a16eeb635c8c110b66e5df3512f67564ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 15 Sep 2022 17:48:22 +0200 Subject: [PATCH 52/92] change using findUserByEmail() instead of raw queries --- backend/src/seeds/factory/creation.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/backend/src/seeds/factory/creation.ts b/backend/src/seeds/factory/creation.ts index 99fd39d3b..606bac1f7 100644 --- a/backend/src/seeds/factory/creation.ts +++ b/backend/src/seeds/factory/creation.ts @@ -8,7 +8,7 @@ import { CreationInterface } from '@/seeds/creation/CreationInterface' import { ApolloServerTestClient } from 'apollo-server-testing' import { Transaction } from '@entity/Transaction' import { Contribution } from '@entity/Contribution' -import { UserContact } from '@entity/UserContact' +import { findUserByEmail } from '@/graphql/resolver/UserResolver' // import CONFIG from '@/config/index' export const nMonthsBefore = (date: Date, months = 1): string => { @@ -27,12 +27,7 @@ export const creationFactory = async ( await mutate({ mutation: adminCreateContribution, variables: { ...creation } }) logger.trace('creationFactory... after adminCreateContribution') - const userContact = await UserContact.findOneOrFail({ - where: { email: creation.email }, - relations: ['user'], - }) - logger.trace('creationFactory... after UserContact.findOneOrFail userContact=', userContact) - const user = userContact.user + const user = await findUserByEmail(creation.email) // userContact.user const pendingCreation = await Contribution.findOneOrFail({ where: { userId: user.id, amount: creation.amount }, From e3e5fdd12b4566287bd9e28b8761c73b8ef22abd Mon Sep 17 00:00:00 2001 From: ogerly Date: Fri, 16 Sep 2022 08:22:24 +0200 Subject: [PATCH 53/92] remove modal box if redeem a redeemlink --- .../LinkInformations/RedeemValid.vue | 2 +- frontend/src/pages/TransactionLink.spec.js | 56 +++++++++---------- frontend/src/pages/TransactionLink.vue | 7 +-- 3 files changed, 30 insertions(+), 35 deletions(-) diff --git a/frontend/src/components/LinkInformations/RedeemValid.vue b/frontend/src/components/LinkInformations/RedeemValid.vue index 353fefaf8..c468a396a 100644 --- a/frontend/src/components/LinkInformations/RedeemValid.vue +++ b/frontend/src/components/LinkInformations/RedeemValid.vue @@ -3,7 +3,7 @@
- + {{ $t('gdd_per_link.redeem') }}
diff --git a/frontend/src/pages/TransactionLink.spec.js b/frontend/src/pages/TransactionLink.spec.js index b1bbd8950..fd58d6e1c 100644 --- a/frontend/src/pages/TransactionLink.spec.js +++ b/frontend/src/pages/TransactionLink.spec.js @@ -282,18 +282,18 @@ describe('TransactionLink', () => { }) describe('redeem link with success', () => { - let spy + // let spy beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + // spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') apolloMutateMock.mockResolvedValue() - spy.mockImplementation(() => Promise.resolve(true)) + // spy.mockImplementation(() => Promise.resolve(true)) await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') }) - it('opens the modal', () => { - expect(spy).toBeCalledWith('gdd_per_link.redeem-text') - }) + // it('opens the modal', () => { + // expect(spy).toBeCalledWith('gdd_per_link.redeem-text') + // }) it('calls the API', () => { expect(apolloMutateMock).toBeCalledWith( @@ -316,37 +316,37 @@ describe('TransactionLink', () => { }) }) - describe('cancel redeem link', () => { - let spy + // describe('cancel redeem link', () => { + // let spy - beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - apolloMutateMock.mockResolvedValue() - spy.mockImplementation(() => Promise.resolve(false)) - await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') - }) + // beforeEach(async () => { + // spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + // apolloMutateMock.mockResolvedValue() + // spy.mockImplementation(() => Promise.resolve(false)) + // await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') + // }) - it('does not call the API', () => { - expect(apolloMutateMock).not.toBeCalled() - }) + // it('does not call the API', () => { + // expect(apolloMutateMock).not.toBeCalled() + // }) - it('does not toasts a success message', () => { - expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) - expect(toastSuccessSpy).not.toBeCalled() - }) + // it('does not toasts a success message', () => { + // expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) + // expect(toastSuccessSpy).not.toBeCalled() + // }) - it('does not push the route', () => { - expect(routerPushMock).not.toBeCalled() - }) - }) + // it('does not push the route', () => { + // expect(routerPushMock).not.toBeCalled() + // }) + // }) describe('redeem link with error', () => { - let spy + // let spy beforeEach(async () => { - spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') + // spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) - spy.mockImplementation(() => Promise.resolve(true)) + // spy.mockImplementation(() => Promise.resolve(true)) await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') }) diff --git a/frontend/src/pages/TransactionLink.vue b/frontend/src/pages/TransactionLink.vue index 57236b55c..bd1909d7d 100644 --- a/frontend/src/pages/TransactionLink.vue +++ b/frontend/src/pages/TransactionLink.vue @@ -14,7 +14,7 @@ @@ -98,11 +98,6 @@ export default { this.$router.push('/overview') }) }, - redeemLink(amount) { - this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then((value) => { - if (value) this.mutationLink(amount) - }) - }, }, computed: { isContributionLink() { From caf487d2062667ac77794d0ea8624362b7737924 Mon Sep 17 00:00:00 2001 From: ogerly Date: Fri, 16 Sep 2022 08:24:31 +0200 Subject: [PATCH 54/92] remove text from modal for redeem link --- frontend/src/locales/de.json | 1 - frontend/src/locales/en.json | 1 - frontend/src/locales/es.json | 1 - frontend/src/locales/fr.json | 1 - frontend/src/locales/nl.json | 1 - frontend/src/pages/TransactionLink.spec.js | 34 ---------------------- 6 files changed, 39 deletions(-) diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index e6b12b0fa..1ed8af19f 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -178,7 +178,6 @@ "no-redeem": "Du darfst deinen eigenen Link nicht einlösen!", "not-copied": "Dein Gerät lässt das Kopieren leider nicht zu! Bitte kopiere den Link von Hand!", "redeem": "Einlösen", - "redeem-text": "Willst du den Betrag jetzt einlösen?", "redeemed": "Erfolgreich eingelöst! Deinem Konto wurden {n} GDD gutgeschrieben.", "redeemed-at": "Der Link wurde bereits am {date} eingelöst.", "redeemed-title": "eingelöst", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 07125eceb..113fa1cb9 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -178,7 +178,6 @@ "no-redeem": "You not allowed to redeem your own link!", "not-copied": "Unfortunately, your device does not allow copying! Please copy the link by hand!", "redeem": "Redeem", - "redeem-text": "Do you want to redeem the amount now?", "redeemed": "Successfully redeemed! Your account has been credited with {n} GDD.", "redeemed-at": "The link was already redeemed on {date}.", "redeemed-title": "redeemed", diff --git a/frontend/src/locales/es.json b/frontend/src/locales/es.json index 987bb71ef..b2a229f1c 100644 --- a/frontend/src/locales/es.json +++ b/frontend/src/locales/es.json @@ -180,7 +180,6 @@ "no-redeem": "No puedes canjear tu propio enlace!", "not-copied": "¡Desafortunadamente, su dispositivo no permite copiar! Copie el enlace manualmente!", "redeem": "Canjear", - "redeem-text": "¿Quieres canjear el importe ahora?", "redeemed": "¡Canjeado con éxito! Tu cuenta ha sido acreditada con {n} GDD.", "redeemed-at": "El enlace ya se canjeó el {date}.", "redeemed-title": "canjeado", diff --git a/frontend/src/locales/fr.json b/frontend/src/locales/fr.json index 61037af1a..e7ac18a18 100644 --- a/frontend/src/locales/fr.json +++ b/frontend/src/locales/fr.json @@ -180,7 +180,6 @@ "no-redeem": "Vous n´êtes pas autorisé à percevoir votre propre lien!", "not-copied": "Malheureusement votre appareil ne permet pas de copier! Veuillez copier le lien manuellement svp!", "redeem": "Encaisser", - "redeem-text": "Voulez-vous percevoir le montant maintenant?", "redeemed": "Encaissé avec succès! Votre compte est crédité de {n} GDD.", "redeemed-at": "Le lien a déjà été perçu le {date}.", "redeemed-title": "encaisser", diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 99b972514..34f484fd7 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -180,7 +180,6 @@ "no-redeem": "Je mag je eigen link niet inwisselen!", "not-copied": "Jouw apparaat laat het kopiëren helaas niet toe! Kopieer de link alsjeblieft met de hand!", "redeem": "Inwisselen", - "redeem-text": "Wil je het bedrag nu inwisselen?", "redeemed": "Succesvol ingewisseld! Op jouw rekening werden {n} GDD bijgeschreven.", "redeemed-at": "De link werd al op {date} ingewisseld.", "redeemed-title": "ingewisseld", diff --git a/frontend/src/pages/TransactionLink.spec.js b/frontend/src/pages/TransactionLink.spec.js index fd58d6e1c..831502acd 100644 --- a/frontend/src/pages/TransactionLink.spec.js +++ b/frontend/src/pages/TransactionLink.spec.js @@ -282,19 +282,12 @@ describe('TransactionLink', () => { }) describe('redeem link with success', () => { - // let spy beforeEach(async () => { - // spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') apolloMutateMock.mockResolvedValue() - // spy.mockImplementation(() => Promise.resolve(true)) await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') }) - // it('opens the modal', () => { - // expect(spy).toBeCalledWith('gdd_per_link.redeem-text') - // }) - it('calls the API', () => { expect(apolloMutateMock).toBeCalledWith( expect.objectContaining({ @@ -316,37 +309,10 @@ describe('TransactionLink', () => { }) }) - // describe('cancel redeem link', () => { - // let spy - - // beforeEach(async () => { - // spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') - // apolloMutateMock.mockResolvedValue() - // spy.mockImplementation(() => Promise.resolve(false)) - // await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') - // }) - - // it('does not call the API', () => { - // expect(apolloMutateMock).not.toBeCalled() - // }) - - // it('does not toasts a success message', () => { - // expect(mocks.$t).not.toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) - // expect(toastSuccessSpy).not.toBeCalled() - // }) - - // it('does not push the route', () => { - // expect(routerPushMock).not.toBeCalled() - // }) - // }) - describe('redeem link with error', () => { - // let spy beforeEach(async () => { - // spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm') apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) - // spy.mockImplementation(() => Promise.resolve(true)) await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') }) From 222d8c2f579a936221c362a8f05921fc9c7fe359 Mon Sep 17 00:00:00 2001 From: ogerly Date: Fri, 16 Sep 2022 08:25:55 +0200 Subject: [PATCH 55/92] fix lint --- frontend/src/pages/TransactionLink.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/pages/TransactionLink.spec.js b/frontend/src/pages/TransactionLink.spec.js index 831502acd..d5a75aa4a 100644 --- a/frontend/src/pages/TransactionLink.spec.js +++ b/frontend/src/pages/TransactionLink.spec.js @@ -282,7 +282,6 @@ describe('TransactionLink', () => { }) describe('redeem link with success', () => { - beforeEach(async () => { apolloMutateMock.mockResolvedValue() await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') @@ -310,7 +309,6 @@ describe('TransactionLink', () => { }) describe('redeem link with error', () => { - beforeEach(async () => { apolloMutateMock.mockRejectedValue({ message: 'Oh Noo!' }) await wrapper.findComponent({ name: 'RedeemValid' }).find('button').trigger('click') From b8442e172565baa6120a6fdee55458e61b6484fb Mon Sep 17 00:00:00 2001 From: joseji Date: Fri, 16 Sep 2022 13:42:45 +0200 Subject: [PATCH 56/92] added redeem register event test, transaction link execution failing --- .../src/graphql/resolver/UserResolver.test.ts | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 2c6406939..1103f8bf2 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -24,6 +24,8 @@ import { EventProtocol } from '@entity/EventProtocol' import { logger } from '@test/testSetup' import { validate as validateUUID, version as versionUUID } from 'uuid' import { peterLustig } from '@/seeds/users/peter-lustig' +import { TransactionLink } from '@entity/TransactionLink' +import { transactionLinkFactory } from '@/seeds/factory/transactionLink' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -261,13 +263,19 @@ describe('UserResolver', () => { const peter = await User.findOneOrFail({ id: user[0].id }) peter.isAdmin = new Date() await peter.save() + + // date statement + const actualDate = new Date() + const futureDate = new Date() // Create a future day from the executed day + futureDate.setDate(futureDate.getDate() + 1) + // factory logs in as Peter Lustig link = await contributionLinkFactory(testEnv, { name: 'Dokumenta 2022', memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', amount: 200, - validFrom: new Date(2022, 5, 18), - validTo: new Date(2022, 8, 25), + validFrom: actualDate, + validTo: futureDate, }) resetToken() await mutate({ @@ -292,6 +300,39 @@ describe('UserResolver', () => { }), ) }) + + it('stores the redeem register event in the database', () => { + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.REDEEM_REGISTER, + userId: expect.any(Number), // as it is randomly generated + }), + ) + }) + }) + + describe('transaction link', () => { + beforeAll(async () => { + await transactionLinkFactory(testEnv, { + email: 'peter@lustig.de', + amount: 19.99, + memo: `Kein Trick, keine Zauberrei, + bei Gradidio sei dabei!`, + }) + + const transactionLink = await TransactionLink.findOneOrFail() + resetToken() + await mutate({ + mutation: createUser, + variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code }, + }) + }) + + it('sets the referrer id to Peter Lustigs id', () => { + expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual( + expect.objectContaining({ referrerId: user[0].id }), + ) + }) }) /* A transaction link requires GDD on account From 3fcc62539ad6e75b5d6c896bc07624bdb5249838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 16 Sep 2022 15:14:51 +0200 Subject: [PATCH 57/92] changes after merge from master --- backend/src/graphql/model/User.ts | 2 +- .../graphql/resolver/AdminResolver.test.ts | 3 ++- backend/src/graphql/resolver/AdminResolver.ts | 23 ++++++++++++++----- .../resolver/ContributionMessageResolver.ts | 2 +- .../graphql/resolver/TransactionResolver.ts | 5 ++-- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/backend/src/graphql/model/User.ts b/backend/src/graphql/model/User.ts index 8d56c6775..cc52ff1f1 100644 --- a/backend/src/graphql/model/User.ts +++ b/backend/src/graphql/model/User.ts @@ -45,7 +45,7 @@ export class User { emailId: number | null // TODO privacy issue here - @Field(() => String) + @Field(() => String, { nullable: true }) email: string @Field(() => UserContact) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 27e3c6385..2f72155de 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1498,7 +1498,7 @@ describe('AdminResolver', () => { }) // In the futrue this should not throw anymore - it('throws an error for the second confirmation', async () => { + it('and throws an error for the second confirmation', async () => { const r1 = mutate({ mutation: confirmContribution, variables: { @@ -1518,6 +1518,7 @@ describe('AdminResolver', () => { ) await expect(r2).resolves.toEqual( expect.objectContaining({ + // data: { confirmContribution: true }, errors: [new GraphQLError('Creation was not successful.')], }), ) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index 7bdc4d59b..aaf3ed91f 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -426,7 +426,10 @@ export class AdminResolver { logger.error('Moderator can not confirm own contribution') throw new Error('Moderator can not confirm own contribution') } - const user = await dbUser.findOneOrFail({ id: contribution.userId }, { withDeleted: true }) + const user = await dbUser.findOneOrFail( + { id: contribution.userId }, + { withDeleted: true, relations: ['emailContact'] }, + ) if (user.deletedAt) { logger.error('This user was deleted. Cannot confirm a contribution.') throw new Error('This user was deleted. Cannot confirm a contribution.') @@ -438,7 +441,7 @@ export class AdminResolver { const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') try { const lastTransaction = await queryRunner.manager .createQueryBuilder() @@ -487,7 +490,7 @@ export class AdminResolver { senderLastName: moderatorUser.lastName, recipientFirstName: user.firstName, recipientLastName: user.lastName, - recipientEmail: user.email, + recipientEmail: user.emailContact.email, contributionMemo: contribution.memo, contributionAmount: contribution.amount, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, @@ -733,6 +736,9 @@ export class AdminResolver { @Ctx() context: Context, ): Promise { const user = getUser(context) + if (!user.emailContact) { + user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } }) + } const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('READ UNCOMMITTED') @@ -748,6 +754,11 @@ export class AdminResolver { if (contribution.userId === user.id) { throw new Error('Admin can not answer on own contribution') } + if (!contribution.user.emailContact) { + contribution.user.emailContact = await UserContact.findOneOrFail({ + where: { id: contribution.user.emailId }, + }) + } contributionMessage.contributionId = contributionId contributionMessage.createdAt = new Date() contributionMessage.message = message @@ -764,19 +775,19 @@ export class AdminResolver { contribution.contributionStatus = ContributionStatus.IN_PROGRESS await queryRunner.manager.update(Contribution, { id: contributionId }, contribution) } - await queryRunner.commitTransaction() await sendAddedContributionMessageEmail({ senderFirstName: user.firstName, senderLastName: user.lastName, recipientFirstName: contribution.user.firstName, recipientLastName: contribution.user.lastName, - recipientEmail: contribution.user.email, - senderEmail: user.email, + recipientEmail: contribution.user.emailContact.email, + senderEmail: user.emailContact.email, contributionMemo: contribution.memo, message, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) + await queryRunner.commitTransaction() } catch (e) { await queryRunner.rollbackTransaction() logger.error(`ContributionMessage was not successful: ${e}`) diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index fb92806d0..bd2bdeec8 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -23,7 +23,7 @@ export class ContributionMessageResolver { const user = getUser(context) const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('READ COMMITTED') const contributionMessage = DbContributionMessage.create() try { const contribution = await Contribution.findOne({ id: contributionId }) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 4b46804f1..dff5eccf1 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -38,7 +38,6 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' -import { UserContact } from '@entity/UserContact' import { findUserByEmail } from './UserResolver' export const executeTransaction = async ( @@ -165,8 +164,8 @@ export const executeTransaction = async ( senderLastName: recipient.lastName, recipientFirstName: sender.firstName, recipientLastName: sender.lastName, - email: sender.email, - senderEmail: recipient.email, + email: sender.emailContact.email, + senderEmail: recipient.emailContact.email, amount, memo, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, From 86b12f98ed8ec7b7aa21cfc866013dd27c5c0c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 16 Sep 2022 15:20:51 +0200 Subject: [PATCH 58/92] set overall resolvers the isolationlevel to REPEATABLE READ --- backend/src/graphql/resolver/AdminResolver.ts | 2 +- backend/src/graphql/resolver/ContributionMessageResolver.ts | 2 +- backend/src/graphql/resolver/TransactionLinkResolver.ts | 2 +- backend/src/graphql/resolver/TransactionResolver.ts | 2 +- backend/src/graphql/resolver/UserResolver.ts | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index aaf3ed91f..e9ee0b55b 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -741,7 +741,7 @@ export class AdminResolver { } const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') const contributionMessage = DbContributionMessage.create() try { const contribution = await Contribution.findOne({ diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.ts b/backend/src/graphql/resolver/ContributionMessageResolver.ts index bd2bdeec8..0b33c4722 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.ts @@ -23,7 +23,7 @@ export class ContributionMessageResolver { const user = getUser(context) const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ COMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') const contributionMessage = DbContributionMessage.create() try { const contribution = await Contribution.findOne({ id: contributionId }) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index 194126f3f..d8be2d552 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -178,7 +178,7 @@ export class TransactionLinkResolver { logger.info('redeem contribution link...') const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('SERIALIZABLE') + await queryRunner.startTransaction('REPEATABLE READ') try { const contributionLink = await queryRunner.manager .createQueryBuilder() diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index dff5eccf1..7d6d3b31a 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -82,7 +82,7 @@ export const executeTransaction = async ( const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') logger.debug(`open Transaction to write...`) try { // transaction diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 19829c95f..f589e1b35 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -498,7 +498,7 @@ export class UserResolver { const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') try { dbUser = await queryRunner.manager.save(dbUser).catch((error) => { logger.error('Error while saving dbUser', error) @@ -686,7 +686,7 @@ export class UserResolver { const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') try { // Save user @@ -812,7 +812,7 @@ export class UserResolver { const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') + await queryRunner.startTransaction('REPEATABLE READ') try { await queryRunner.manager.save(userEntity).catch((error) => { From 8c38116bde8b0fd2312d56e9eead46796e1185e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 16 Sep 2022 16:52:19 +0200 Subject: [PATCH 59/92] add TEST_MODUS-switch and EMAIL_TEST_RECEIVER --- backend/src/config/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 6a2ebba87..fd3dd1f8e 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -15,6 +15,7 @@ const constants = { LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', + TEST_MODUS: process.env.TEST_MODUS === 'true' || 'false', CONFIG_VERSION: { DEFAULT: 'DEFAULT', EXPECTED: 'v9.2022-07-07', @@ -67,6 +68,7 @@ const loginServer = { const email = { EMAIL: process.env.EMAIL === 'true' || false, + EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'test_team@gradido.net', EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email', EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx', From 4f979991955331d12b18512a71f6cff1760315c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 16 Sep 2022 16:52:54 +0200 Subject: [PATCH 60/92] add prevention to run production in test-mode --- backend/src/server/createServer.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index a0b294281..7652729e9 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -75,6 +75,13 @@ const createServer = async ( logger, }) apollo.applyMiddleware({ app, path: '/' }) + logger.info( + `running in environment PRODUCTION = ${CONFIG.PRODUCTION} with TEST_MODUS = ${CONFIG.TEST_MODUS} ...`, + ) + if (CONFIG.PRODUCTION && CONFIG.TEST_MODUS === 'true') { + logger.error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) + throw new Error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) + } logger.debug('createServer...successful') return { apollo, app, con } } From f9194beca34c1678b70209e09624ecd324e6fb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 16 Sep 2022 16:53:35 +0200 Subject: [PATCH 61/92] change with test-mode = on the email-receiver to EMAIL_TEST_RECEIVER --- backend/src/mailer/sendEMail.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/backend/src/mailer/sendEMail.ts b/backend/src/mailer/sendEMail.ts index 746daa810..02d9a01c1 100644 --- a/backend/src/mailer/sendEMail.ts +++ b/backend/src/mailer/sendEMail.ts @@ -19,6 +19,12 @@ export const sendEMail = async (emailDef: { logger.info(`Emails are disabled via config...`) return false } + if (CONFIG.TEST_MODUS) { + logger.info( + `Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, + ) + emailDef.to = CONFIG.EMAIL_TEST_RECEIVER + } const transporter = createTransport({ host: CONFIG.EMAIL_SMTP_URL, port: Number(CONFIG.EMAIL_SMTP_PORT), From 3afc8c65c36419994eb8cdecc717fddfd15dc3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Mon, 19 Sep 2022 17:47:46 +0200 Subject: [PATCH 62/92] change in one testcase mail.to to CONFIG.EMAIL_TEST_RECEIVER --- backend/src/mailer/sendEMail.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/mailer/sendEMail.test.ts b/backend/src/mailer/sendEMail.test.ts index 5746f1ead..a6f4bb62a 100644 --- a/backend/src/mailer/sendEMail.test.ts +++ b/backend/src/mailer/sendEMail.test.ts @@ -73,7 +73,7 @@ describe('sendEMail', () => { it('calls sendMail of transporter', () => { expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({ from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: 'receiver@mail.org', + to: `${CONFIG.EMAIL_TEST_RECEIVER}`, cc: 'support@gradido.net', subject: 'Subject', text: 'Text text text', From a9cc4e9339d2539f9654fd6ef45f6698030ebb4c Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Mon, 19 Sep 2022 18:10:11 +0200 Subject: [PATCH 63/92] remove value column --- .../BusinessEventProtocol.md | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md b/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md index f8cf2cd03..58fe01fa0 100644 --- a/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md +++ b/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md @@ -6,30 +6,30 @@ With the business event protocol the gradido application will capture and persis The different event types will be defined as Enum. The following list is a first draft and will grow with further event types in the future. -| EventType | Value | Description | -| ----------------------------------- | ----- | ------------------------------------------------------------------------------------------------------ | -| BasicEvent | 0 | the basic event is the root of all further extending event types | -| VisitGradidoEvent | 10 | if a user visits a gradido page without login or register | -| RegisterEvent | 20 | the user presses the register button | -| RedeemRegisterEvent | 21 | the user presses the register button initiated by the redeem link | -| InActiveAccountEvent | 22 | the systems create an inactive account during the register process | -| SendConfirmEmailEvent | 23 | the system send a confirmation email to the user during the register process | -| ConfirmEmailEvent | 24 | the user confirms his email during the register process | -| RegisterEmailKlickTippEvent | 25 | the system registers the confirmed email at klicktipp | -| LoginEvent | 30 | the user presses the login button | -| RedeemLoginEvent | 31 | the user presses the login button initiated by the redeem link | -| ActivateAccountEvent | 32 | the system activates the users account during the first login process | -| PasswordChangeEvent | 33 | the user changes his password | -| TransactionSendEvent | 40 | the user creates a transaction and sends it online | -| TransactionSendRedeemEvent | 41 | the user creates a transaction and sends it per redeem link | -| TransactionRepeateRedeemEvent | 42 | the user recreates a redeem link of a still open transaction | -| TransactionCreationEvent | 50 | the user receives a creation transaction for his confirmed contribution | -| TransactionReceiveEvent | 51 | the user receives a transaction from an other user and posts the amount on his account | -| TransactionReceiveRedeemEvent | 52 | the user activates the redeem link and receives the transaction and posts the amount on his account | -| ContributionCreateEvent | 60 | the user enters his contribution and asks for confirmation | -| ContributionConfirmEvent | 61 | the user confirms a contribution of an other user (for future multi confirmation from several users) | -| ContributionLinkDefineEvent | 70 | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium | -| ContributionLinkActivateRedeemEvent | 71 | the user activates a received contributionLink to create a contribution entry for the contributionLink | +| EventType | Description | +| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | +| BasicEvent | the basic event is the root of all further extending event types | +| VisitGradidoEvent | if a user visits a gradido page without login or register | +| RegisterEvent | the user presses the register button | +| RedeemRegisterEvent | the user presses the register button initiated by the redeem link | +| InActiveAccountEvent | the systems create an inactive account during the register process | +| SendConfirmEmailEvent | the system send a confirmation email to the user during the register process | +| ConfirmEmailEvent | the user confirms his email during the register process | +| RegisterEmailKlickTippEvent | the system registers the confirmed email at klicktipp | +| LoginEvent | the user presses the login button | +| RedeemLoginEvent | the user presses the login button initiated by the redeem link | +| ActivateAccountEvent | the system activates the users account during the first login process | +| PasswordChangeEvent | the user changes his password | +| TransactionSendEvent | the user creates a transaction and sends it online | +| TransactionSendRedeemEvent | the user creates a transaction and sends it per redeem link | +| TransactionRepeateRedeemEvent | the user recreates a redeem link of a still open transaction | +| TransactionCreationEvent | the user receives a creation transaction for his confirmed contribution | +| TransactionReceiveEvent | the user receives a transaction from an other user and posts the amount on his account | +| TransactionReceiveRedeemEvent | the user activates the redeem link and receives the transaction and posts the amount on his account | +| ContributionCreateEvent | the user enters his contribution and asks for confirmation | +| ContributionConfirmEvent | the user confirms a contribution of an other user (for future multi confirmation from several users) | +| ContributionLinkDefineEvent | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium | +| ContributionLinkActivateRedeemEvent | the user activates a received contributionLink to create a contribution entry for the contributionLink | ## EventProtocol - Entity From 70963203a001d868f0ac6366f34ae581ff5e5605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 00:39:45 +0200 Subject: [PATCH 64/92] rework Event definitions --- .../BusinessEventProtocol.md | 121 +++++++++++------- 1 file changed, 72 insertions(+), 49 deletions(-) diff --git a/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md b/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md index 58fe01fa0..ecdf8df34 100644 --- a/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md +++ b/docu/Concepts/TechnicalRequirements/BusinessEventProtocol.md @@ -6,30 +6,41 @@ With the business event protocol the gradido application will capture and persis The different event types will be defined as Enum. The following list is a first draft and will grow with further event types in the future. -| EventType | Description | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------ | -| BasicEvent | the basic event is the root of all further extending event types | -| VisitGradidoEvent | if a user visits a gradido page without login or register | -| RegisterEvent | the user presses the register button | -| RedeemRegisterEvent | the user presses the register button initiated by the redeem link | -| InActiveAccountEvent | the systems create an inactive account during the register process | -| SendConfirmEmailEvent | the system send a confirmation email to the user during the register process | -| ConfirmEmailEvent | the user confirms his email during the register process | -| RegisterEmailKlickTippEvent | the system registers the confirmed email at klicktipp | -| LoginEvent | the user presses the login button | -| RedeemLoginEvent | the user presses the login button initiated by the redeem link | -| ActivateAccountEvent | the system activates the users account during the first login process | -| PasswordChangeEvent | the user changes his password | -| TransactionSendEvent | the user creates a transaction and sends it online | -| TransactionSendRedeemEvent | the user creates a transaction and sends it per redeem link | -| TransactionRepeateRedeemEvent | the user recreates a redeem link of a still open transaction | -| TransactionCreationEvent | the user receives a creation transaction for his confirmed contribution | -| TransactionReceiveEvent | the user receives a transaction from an other user and posts the amount on his account | -| TransactionReceiveRedeemEvent | the user activates the redeem link and receives the transaction and posts the amount on his account | -| ContributionCreateEvent | the user enters his contribution and asks for confirmation | -| ContributionConfirmEvent | the user confirms a contribution of an other user (for future multi confirmation from several users) | -| ContributionLinkDefineEvent | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium | -| ContributionLinkActivateRedeemEvent | the user activates a received contributionLink to create a contribution entry for the contributionLink | +| EventType | Description | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| BasicEvent | the basic event is the root of all further extending event types | +| VisitGradidoEvent | if a user visits a gradido page without login or register; possible as soon as a request-response-loop for the first page will be invoked | +| RegisterEvent | the user presses the register button | +| LoginEvent | the user presses the login button | +| VerifyRedeemEvent | the user presses a redeem link independent from transaction or contribution redeem | +| RedeemRegisterEvent | the user presses the register-button initiated by the redeem link | +| RedeemLoginEvent | the user presses the login-button initiated by the redeem link | +| ActivateAccountEvent | the system activates the users account after a successful confirmEmail-Event or during a reactivation of a deactivated account | +| InActiveAccountEvent | the systems creates an inactive account during the register process or an active account will be reset to inactive | +| SetPasswordEvent | the system sets a new password after ConfirmEmailEvent or SendForgotPasswordEvent | +| RegisterEmailKlickTippEvent | the system registers the confirmed email at klicktipp | +| PasswordChangeEvent | the user changes his password in his Profile | +| TransactionSendEvent | the user creates a transaction and sends it online; paired with TransactionReceiveEvent | +| TransactionLinkCreateEvent | the user creates a transaction link | +| TransactionReceiveEvent | the user receives a transaction from an other user and posts the amount on his account; paired with TransactionSendEvent | +| TransactionLinkRedeemEvent | the user activates the redeem link and receives the transaction and posts the amount on his account | +| ContributionCreateEvent | the user enters his contribution and asks for confirmation | +| ContributionConfirmEvent | the admin user confirms a contribution of an other user (for future multi confirmation from several users) | +| ContributionDenyEvent | the admin user denies a contribution of an other user | +| ContributionLinkDefineEvent | the admin user defines a contributionLink, which could be send per Link/QR-Code on an other medium | +| ContributionLinkRedeemEvent | the user activates a received contributionLink to create a contribution entry for the contributionLink | +| UserCreateContributionMessageEvent | the user captures a new message for a contribution | +| AdminCreateContributionMessageEvent | the admin user captures a new message for a contribution | +| LogoutEvent | the user invokes a logout | +| SendConfirmEmailEvent | the system sends a confirmation email to the user during the registration process | +| SendAccountMultiRegistrationEmailEvent | the system sends a info email to the user, that an other user tries to register with his existing email address | +| SendForgotPasswordEmailEvent | the system sends the forgot password email including a special link to start the forgot password process | +| SendTransactionSendEmailEvent | the system sends an email to inform the user about his transaction was sent to an other user | +| SendTransactionReceiveEmailEvent | the system sends an email to inform the user about a received transaction from an other user | +| SendAddedContributionEmailEvent | the system sends an email to inform the user about the creation of his captured contribution | +| SendContributionConfirmEmailEvent | the system sends an email to inform the user about the confirmation of his contribution | +| SendTransactionLinkRedeemEmailEvent | the system sends an email to the user, who created the transactionlink, that the link was redeemed | +| | | ## EventProtocol - Entity @@ -49,32 +60,44 @@ The business events will be stored in database in the new table `EventProtocol`. ## Event Types -The following table lists for each event type the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table: +The following table lists for each event type the mapping between old and new key, the mandatory attributes, which have to be initialized at event occurence and to be written in the database event protocol table: -| EventType | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount | -| :---------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: | -| BasicEvent | x | x | x | | | | | | | -| VisitGradidoEvent | x | x | x | | | | | | | -| RegisterEvent | x | x | x | x | | | | | | -| RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | | -| InActiveAccountEvent | x | x | x | x | | | | | | -| SendConfirmEmailEvent | x | x | x | x | | | | | | -| ConfirmEmailEvent | x | x | x | x | | | | | | -| RegisterEmailKlickTippEvent | x | x | x | x | | | | | | -| LoginEvent | x | x | x | x | | | | | | -| RedeemLoginEvent | x | x | x | x | | | (x) | (x) | | -| ActivateAccountEvent | x | x | x | x | | | | | | -| PasswordChangeEvent | x | x | x | x | | | | | | -| TransactionSendEvent | x | x | x | x | x | x | x | | x | -| TransactionSendRedeemEvent | x | x | x | x | x | x | x | | x | -| TransactionRepeateRedeemEvent | x | x | x | x | x | x | x | | x | -| TransactionCreationEvent | x | x | x | x | | | x | | x | -| TransactionReceiveEvent | x | x | x | x | x | x | x | | x | -| TransactionReceiveRedeemEvent | x | x | x | x | x | x | x | | x | -| ContributionCreateEvent | x | x | x | x | | | | x | x | -| ContributionConfirmEvent | x | x | x | x | x | x | | x | x | -| ContributionLinkDefineEvent | x | x | x | x | | | | | x | -| ContributionLinkActivateRedeemEvent | x | x | x | x | | | | x | x | +| EventType - old key | EventType - new key | id | type | createdAt | userID | XuserID | XCommunityID | transactionID | contribID | amount | +| :-------------------------------- | :------------------------------------- | :-: | :--: | :-------: | :----: | :-----: | :----------: | :-----------: | :-------: | :----: | +| BASIC | BasicEvent | x | x | x | | | | | | | +| VISIT_GRADIDO | VisitGradidoEvent | x | x | x | | | | | | | +| REGISTER | RegisterEvent | x | x | x | x | | | | | | +| LOGIN | LoginEvent | x | x | x | x | | | | | | +| | VerifyRedeemEvent | | | | | | | | | | +| REDEEM_REGISTER | RedeemRegisterEvent | x | x | x | x | | | (x) | (x) | | +| REDEEM_LOGIN | RedeemLoginEvent | x | x | x | x | | | (x) | (x) | | +| ACTIVATE_ACCOUNT | ActivateAccountEvent | x | x | x | x | | | | | | +| INACTIVE_ACCOUNT | InActiveAccountEvent | x | x | x | x | | | | | | +| CONFIRM_EMAIL | SetPasswordEvent | x | x | x | x | | | | | | +| REGISTER_EMAIL_KLICKTIPP | RegisterEmailKlickTippEvent | x | x | x | x | | | | | | +| PASSWORD_CHANGE | PasswordChangeEvent | x | x | x | x | | | | | | +| TRANSACTION_SEND | TransactionSendEvent | x | x | x | x | x | x | x | | x | +| TRANSACTION_CREATION | TransactionLinkCreateEvent | x | x | x | x | | | x | | x | +| TRANSACTION_RECEIVE | TransactionReceiveEvent | x | x | x | x | x | x | x | | x | +| TRANSACTION_SEND_REDEEM | TransactionLinkRedeemEvent | x | x | x | x | x | x | x | | x | +| CONTRIBUTION_CREATE | ContributionCreateEvent | x | x | x | x | | | | x | x | +| CONTRIBUTION_CONFIRM | ContributionConfirmEvent | x | x | x | x | x | x | | x | x | +| | ContributionDenyEvent | x | x | x | x | x | x | | x | x | +| CONTRIBUTION_LINK_DEFINE | ContributionLinkDefineEvent | x | x | x | x | | | | | x | +| CONTRIBUTION_LINK_ACTIVATE_REDEEM | ContributionLinkRedeemEvent | x | x | x | x | | | | x | x | +| | UserCreateContributionMessageEvent | x | x | x | x | | | | x | x | +| | AdminCreateContributionMessageEvent | x | x | x | x | | | | x | x | +| | LogoutEvent | x | x | x | x | | | | x | x | +| SEND_CONFIRMATION_EMAIL | SendConfirmEmailEvent | x | x | x | x | | | | | | +| | SendAccountMultiRegistrationEmailEvent | x | x | x | x | | | | | | +| | SendForgotPasswordEmailEvent | x | x | x | x | | | | | | +| | SendTransactionSendEmailEvent | x | x | x | x | x | x | x | | x | +| | SendTransactionReceiveEmailEvent | x | x | x | x | x | x | x | | x | +| | SendAddedContributionEmailEvent | x | x | x | x | | | | x | x | +| | SendContributionConfirmEmailEvent | x | x | x | x | | | | x | x | +| | SendTransactionLinkRedeemEmailEvent | x | x | x | x | x | x | x | | x | +| TRANSACTION_REPEATE_REDEEM | - | | | | | | | | | | +| TRANSACTION_RECEIVE_REDEEM | - | | | | | | | | | | ## Event creation From efb6ad70f65b6b5e00f83d925c6fc18ef86199c5 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:08:05 +0200 Subject: [PATCH 65/92] Update backend/src/config/index.ts Co-authored-by: Moriz Wahl --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index fd3dd1f8e..c4b5d5d69 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -15,7 +15,7 @@ const constants = { LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', - TEST_MODUS: process.env.TEST_MODUS === 'true' || 'false', + EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false', CONFIG_VERSION: { DEFAULT: 'DEFAULT', EXPECTED: 'v9.2022-07-07', From ae3ef7edda2c811e9d27aea9f6a36abe18e919ed Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:08:28 +0200 Subject: [PATCH 66/92] Update backend/src/mailer/sendEMail.ts Co-authored-by: Moriz Wahl --- backend/src/mailer/sendEMail.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/mailer/sendEMail.ts b/backend/src/mailer/sendEMail.ts index 02d9a01c1..00282f232 100644 --- a/backend/src/mailer/sendEMail.ts +++ b/backend/src/mailer/sendEMail.ts @@ -19,7 +19,7 @@ export const sendEMail = async (emailDef: { logger.info(`Emails are disabled via config...`) return false } - if (CONFIG.TEST_MODUS) { + if (CONFIG.EMAIL_TEST_MODUS) { logger.info( `Testmodus=ON: change receiver from ${emailDef.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`, ) From 49e0606d169b9657ac767b1b65a47aa3fcf99886 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:08:44 +0200 Subject: [PATCH 67/92] Update backend/src/server/createServer.ts Co-authored-by: Moriz Wahl --- backend/src/server/createServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 7652729e9..97a8379bf 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -76,7 +76,7 @@ const createServer = async ( }) apollo.applyMiddleware({ app, path: '/' }) logger.info( - `running in environment PRODUCTION = ${CONFIG.PRODUCTION} with TEST_MODUS = ${CONFIG.TEST_MODUS} ...`, + `running in environment PRODUCTION = ${CONFIG.PRODUCTION} with EMAIL_TEST_MODUS = ${CONFIG.EMAIL_TEST_MODUS} ...`, ) if (CONFIG.PRODUCTION && CONFIG.TEST_MODUS === 'true') { logger.error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) From fc706bc030256d09665a9a7184167603981ffa02 Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Tue, 20 Sep 2022 16:08:56 +0200 Subject: [PATCH 68/92] Update backend/src/server/createServer.ts Co-authored-by: Moriz Wahl --- backend/src/server/createServer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 97a8379bf..7479595bc 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -78,7 +78,7 @@ const createServer = async ( logger.info( `running in environment PRODUCTION = ${CONFIG.PRODUCTION} with EMAIL_TEST_MODUS = ${CONFIG.EMAIL_TEST_MODUS} ...`, ) - if (CONFIG.PRODUCTION && CONFIG.TEST_MODUS === 'true') { + if (CONFIG.PRODUCTION && CONFIG.EMAIL_TEST_MODUS === 'true') { logger.error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) throw new Error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) } From ae01f99c36edb310fb7639a0c53aed053741f830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 18:20:42 +0200 Subject: [PATCH 69/92] describe the manual setting of new env variable GRADIDO_ENV_NAME --- deployment/bare_metal/setup.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md index f43a3d655..a88150f95 100644 --- a/deployment/bare_metal/setup.md +++ b/deployment/bare_metal/setup.md @@ -95,5 +95,8 @@ > cp .env.dist .env > nano .env >> Adjust values accordingly +# Define name of gradido environment +> the env variable GRADIDO_ENV_NAME have to be set during system installation manually to "development, stage1, stage2, stage3, production" +> export GRADIDO_ENV_NAME=development # TODO the install.sh is not yet ready to run directly - consider to use it as pattern to do it manually > ./install.sh From 34071fd9b41e206e784b71b0e7799784209fc327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 18:21:24 +0200 Subject: [PATCH 70/92] use the new env variable GRADIDO_ENV_NAME to init NODE_ENV --- deployment/bare_metal/start.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index 95b89241f..8b0277fa7 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -120,8 +120,8 @@ yarn build if [ "$DEPLOY_SEED_DATA" = "true" ]; then yarn seed fi -# TODO maybe handle this differently? -export NODE_ENV=production +# the env variable GRADIDO_ENV_NAME have to be set during system installation manually (development, stage1, stage2, stage3) if it should differ from production +export NODE_ENV="${GRADIDO_ENV_NAME:=production}" pm2 delete gradido-backend pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save @@ -133,8 +133,8 @@ cd $PROJECT_ROOT/frontend unset NODE_ENV yarn install yarn build -# TODO maybe handle this differently? -export NODE_ENV=production +# the env variable GRADIDO_ENV_NAME have to be set during system installation manually (development, stage1, stage2, stage3) if it should differ from production +export NODE_ENV="${GRADIDO_ENV_NAME:=production}" pm2 delete gradido-frontend pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save @@ -146,8 +146,8 @@ cd $PROJECT_ROOT/admin unset NODE_ENV yarn install yarn build -# TODO maybe handle this differently? -export NODE_ENV=production +# the env variable GRADIDO_ENV_NAME have to be set during system installation manually (development, stage1, stage2, stage3) if it should differ from production +export NODE_ENV="${GRADIDO_ENV_NAME:=production}" pm2 delete gradido-admin pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save From 7f818fa77ea57ae426ae059a209e8289fbc25473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 18:26:02 +0200 Subject: [PATCH 71/92] add new const server.ENV_NAME, which will be init by GRADIDO_ENV_NAME in start.sh or by NODE_ENV in .env --- backend/src/config/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index c4b5d5d69..50d68d726 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -29,6 +29,7 @@ const server = { JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', + ENV_NAME: process.env.NODE_ENV || 'production', PRODUCTION: process.env.NODE_ENV === 'production' || false, } From 95b4052ed5bc7c7641acf08ba5c4160925ab8ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 18:26:56 +0200 Subject: [PATCH 72/92] correct and more detailed logoutput --- backend/src/server/createServer.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 7479595bc..3d89f7737 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -76,11 +76,11 @@ const createServer = async ( }) apollo.applyMiddleware({ app, path: '/' }) logger.info( - `running in environment PRODUCTION = ${CONFIG.PRODUCTION} with EMAIL_TEST_MODUS = ${CONFIG.EMAIL_TEST_MODUS} ...`, + `running in GRADIDO_ENV_NAME=${CONFIG.ENV_NAME} as PRODUCTION=${CONFIG.PRODUCTION} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`, ) if (CONFIG.PRODUCTION && CONFIG.EMAIL_TEST_MODUS === 'true') { - logger.error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) - throw new Error(`### RUNNING ENVIRONMENT Production IN TEST_MODE IS NOT ALLOWED ###`) + logger.error(`### RUNNING ENVIRONMENT Production IN EMAIL_TEST_MODE IS NOT ALLOWED ###`) + throw new Error(`### RUNNING ENVIRONMENT Production IN EMAIL_TEST_MODE IS NOT ALLOWED ###`) } logger.debug('createServer...successful') return { apollo, app, con } From 736d8023bbd57813f5df2b1f15c4b9347a1a8d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 19:10:54 +0200 Subject: [PATCH 73/92] add description to create cronjobs for deleting yarn /tmp output --- deployment/bare_metal/install.sh | 4 ++++ deployment/bare_metal/setup.md | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/deployment/bare_metal/install.sh b/deployment/bare_metal/install.sh index 9366cbc75..ddb2706eb 100755 --- a/deployment/bare_metal/install.sh +++ b/deployment/bare_metal/install.sh @@ -131,6 +131,10 @@ envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/frontend/.env # Configure admin envsubst "$(env | sed -e 's/=.*//' -e 's/^/\$/g')" < $PROJECT_ROOT/admin/.env.template > $PROJECT_ROOT/admin/.env +# create cronjob to delete yarn output in /tmp +# crontab -e +# hourly job: 0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null +# daily job: 0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null # Start gradido # Note: on first startup some errors will occur - nothing serious ./start.sh \ No newline at end of file diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md index a88150f95..2f9a702e3 100644 --- a/deployment/bare_metal/setup.md +++ b/deployment/bare_metal/setup.md @@ -97,6 +97,14 @@ >> Adjust values accordingly # Define name of gradido environment > the env variable GRADIDO_ENV_NAME have to be set during system installation manually to "development, stage1, stage2, stage3, production" -> export GRADIDO_ENV_NAME=development +> export GRADIDO_ENV_NAME=development +# Define cronjob to compensate yarn output in /tmp +> yarn creates output in /tmp directory, which must be deleted regularly and will be done per cronjob +> on stage1 a hourly job is necessary by setting the following job in the crontab for the gradido user +> crontab -e opens the crontab in edit-mode and insert the following entry: +> "0 * * * * find /tmp -name "yarn--*" -cmin +60 -exec rm -r {} \; > /dev/null" +> on stage2 a daily job is necessary by setting the following job in the crontab for the gradido user +> crontab -e opens the crontab in edit-mode and insert the following entry: +> "0 4 * * * find /tmp -name "yarn--*" -ctime +1 -exec rm -r {} \; > /dev/null" # TODO the install.sh is not yet ready to run directly - consider to use it as pattern to do it manually > ./install.sh From d29d9bd2badfa9af4824b730a16a7d9ba284318c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Tue, 20 Sep 2022 19:11:57 +0200 Subject: [PATCH 74/92] add property changes in .env files as mentioned in PR --- backend/.env.dist | 4 +++- backend/.env.template | 3 +++ backend/src/config/index.ts | 4 ++-- deployment/bare_metal/.env.dist | 5 ++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 69b1d6a46..06e0c3a64 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -1,4 +1,4 @@ -CONFIG_VERSION=v9.2022-07-07 +CONFIG_VERSION=v10.2022-09-20 # Server PORT=4000 @@ -37,6 +37,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a # EMail EMAIL=false +EMAIL_TEST_MODUS=false +EMAIL_TEST_RECEIVER=test_team@gradido.net EMAIL_USERNAME=gradido_email EMAIL_SENDER=info@gradido.net EMAIL_PASSWORD=xxx diff --git a/backend/.env.template b/backend/.env.template index beaa256ef..bace78673 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -5,6 +5,7 @@ JWT_SECRET=$JWT_SECRET JWT_EXPIRES_IN=$JWT_EXPIRES_IN GRAPHIQL=false GDT_API_URL=$GDT_API_URL +ENV_NAME=$ENV_NAME # Database DB_HOST=localhost @@ -36,6 +37,8 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a # EMail EMAIL=$EMAIL +EMAIL_TEST_MODUS=$EMAIL_TEST_MODUS +EMAIL_TEST_RECEIVER=$EMAIL_TEST_RECEIVER EMAIL_USERNAME=$EMAIL_USERNAME EMAIL_SENDER=$EMAIL_SENDER EMAIL_PASSWORD=$EMAIL_PASSWORD diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 50d68d726..462620b14 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -15,10 +15,9 @@ const constants = { LOG4JS_CONFIG: 'log4js-config.json', // default log level on production should be info LOG_LEVEL: process.env.LOG_LEVEL || 'info', - EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false', CONFIG_VERSION: { DEFAULT: 'DEFAULT', - EXPECTED: 'v9.2022-07-07', + EXPECTED: 'v10.2022-09-20', CURRENT: '', }, } @@ -69,6 +68,7 @@ const loginServer = { const email = { EMAIL: process.env.EMAIL === 'true' || false, + EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false', EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'test_team@gradido.net', EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email', EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', diff --git a/deployment/bare_metal/.env.dist b/deployment/bare_metal/.env.dist index ebb6717e7..b090908e1 100644 --- a/deployment/bare_metal/.env.dist +++ b/deployment/bare_metal/.env.dist @@ -26,10 +26,11 @@ COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code} COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community" # backend -BACKEND_CONFIG_VERSION=v9.2022-07-07 +BACKEND_CONFIG_VERSION=v10.2022-09-20 JWT_EXPIRES_IN=10m GDT_API_URL=https://gdt.gradido.net +ENV_NAME=stage1 TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log @@ -40,6 +41,8 @@ KLICKTIPP_APIKEY_DE= KLICKTIPP_APIKEY_EN= EMAIL=true +EMAIL_TEST_MODUS=false +EMAIL_TEST_RECEIVER=test_team@gradido.net EMAIL_USERNAME=peter@lustig.de EMAIL_SENDER=peter@lustig.de EMAIL_PASSWORD=1234 From bf6ed35c6e1b359599fa6f28483e8e3c8eb65587 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 21 Sep 2022 12:05:49 +0200 Subject: [PATCH 75/92] Change text template of ContributionMessageReceived. --- .../src/mailer/text/contributionMessageReceived.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/backend/src/mailer/text/contributionMessageReceived.ts b/backend/src/mailer/text/contributionMessageReceived.ts index 4affb71a8..b0c9c4d30 100644 --- a/backend/src/mailer/text/contributionMessageReceived.ts +++ b/backend/src/mailer/text/contributionMessageReceived.ts @@ -14,17 +14,15 @@ export const contributionMessageReceived = { }): string => `Hallo ${data.recipientFirstName} ${data.recipientLastName}, -Du hast soeben zu deinem eingereichten Gradido Schöpfungsantrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten. -Die Rückfrage lautet: +du hast soeben zu deinem eingereichten Gemeinwohl-Beitrag "${data.contributionMemo}" eine Rückfrage von ${data.senderFirstName} ${data.senderLastName} erhalten. -${data.message} +Bitte beantworte die Rückfrage in deinem Gradido-Konto im Menü "Gemeinschaft" im Tab "Meine Beiträge zum Gemeinwohl"! + +Link zu deinem Konto: ${data.overviewURL} Bitte antworte nicht auf diese E-Mail! Mit freundlichen Grüßen, -dein Gradido-Team - - -Link zu deinem Konto: ${data.overviewURL}`, +dein Gradido-Team`, }, } From 6c446fb4772d7af635658c6d25e86eb68b9a6029 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 21 Sep 2022 12:07:19 +0200 Subject: [PATCH 76/92] Change text template of transactionReceived. --- backend/src/mailer/text/transactionReceived.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts index 2764d1b60..c533b5b1f 100644 --- a/backend/src/mailer/text/transactionReceived.ts +++ b/backend/src/mailer/text/transactionReceived.ts @@ -19,17 +19,13 @@ export const transactionReceived = { Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${ data.senderLastName } (${data.senderEmail}) erhalten. -${data.senderFirstName} ${data.senderLastName} schreibt: -${data.memo} +Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} Bitte antworte nicht auf diese E-Mail! Mit freundlichen Grüßen, -dein Gradido-Team - - -Link zu deinem Konto: ${data.overviewURL}`, +dein Gradido-Team`, }, } From 3b3a963ac5cc4c1a35b63a45957b71292d31ce89 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 21 Sep 2022 12:09:36 +0200 Subject: [PATCH 77/92] Change text template of contributionConfirmed. --- backend/src/mailer/text/contributionConfirmed.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/backend/src/mailer/text/contributionConfirmed.ts b/backend/src/mailer/text/contributionConfirmed.ts index 2d9fce6a8..dc82d7615 100644 --- a/backend/src/mailer/text/contributionConfirmed.ts +++ b/backend/src/mailer/text/contributionConfirmed.ts @@ -14,9 +14,10 @@ export const contributionConfirmed = { }): string => `Hallo ${data.recipientFirstName} ${data.recipientLastName}, -Dein Gradido Schöpfungsantrag "${data.contributionMemo}" wurde soeben von ${data.senderFirstName} ${ - data.senderLastName - } bestätigt. +Dein eingereichter Gemeinwohl-Beitrag "${data.contributionMemo}" wurde soeben von ${ + data.senderFirstName + } ${data.senderLastName} bestätigt. + Betrag: ${data.contributionAmount.toFixed(2).replace('.', ',')} GDD Bitte antworte nicht auf diese E-Mail! From 686c43fa895ced5ad94042b2469365ccabe35d10 Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 21 Sep 2022 12:12:23 +0200 Subject: [PATCH 78/92] Change text of linkRedeemed template. --- backend/src/mailer/text/transactionReceived.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts index c533b5b1f..4f10f41c8 100644 --- a/backend/src/mailer/text/transactionReceived.ts +++ b/backend/src/mailer/text/transactionReceived.ts @@ -47,15 +47,16 @@ export const transactionLinkRedeemed = { ${data.senderFirstName} ${data.senderLastName} (${ data.senderEmail - }) hat soeben deinen Link eingelösst. + }) hat soeben deinen Link eingelöst. + Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD, Memo: ${data.memo} +Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} + Bitte antworte nicht auf diese E-Mail! Mit freundlichen Grüßen, -dein Gradido-Team - -Link zu deinem Konto: ${data.overviewURL}`, +dein Gradido-Team`, }, } From 6c0cb17196e82119f16e57f9130006361ab112ea Mon Sep 17 00:00:00 2001 From: elweyn Date: Wed, 21 Sep 2022 12:41:25 +0200 Subject: [PATCH 79/92] Refactor sendTransactionLinkredeemed . --- .../graphql/resolver/TransactionResolver.ts | 7 +-- .../sendTransactionLinkRedeemed.test.ts | 44 +++++++++++++++++++ .../src/mailer/sendTransactionLinkRedeemed.ts | 28 ++++++++++++ .../sendTransactionReceivedEmail.test.ts | 2 - .../mailer/sendTransactionReceivedEmail.ts | 27 +----------- .../mailer/text/transactionLinkRedeemed.ts | 33 ++++++++++++++ .../src/mailer/text/transactionReceived.ts | 33 -------------- 7 files changed, 108 insertions(+), 66 deletions(-) create mode 100644 backend/src/mailer/sendTransactionLinkRedeemed.test.ts create mode 100644 backend/src/mailer/sendTransactionLinkRedeemed.ts create mode 100644 backend/src/mailer/text/transactionLinkRedeemed.ts diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 1d8748f58..c192ae9dc 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -8,10 +8,7 @@ import { Context, getUser } from '@/server/context' import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { getCustomRepository, getConnection } from '@dbTools/typeorm' -import { - sendTransactionLinkRedeemedEmail, - sendTransactionReceivedEmail, -} from '@/mailer/sendTransactionReceivedEmail' +import { sendTransactionReceivedEmail } from '@/mailer/sendTransactionReceivedEmail' import { Transaction } from '@model/Transaction' import { TransactionList } from '@model/TransactionList' @@ -38,6 +35,7 @@ import Decimal from 'decimal.js-light' import { BalanceResolver } from './BalanceResolver' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' +import { sendTransactionLinkRedeemedEmail } from '@/mailer/sendTransactionLinkRedeemed' export const executeTransaction = async ( amount: Decimal, @@ -154,7 +152,6 @@ export const executeTransaction = async ( email: recipient.email, senderEmail: sender.email, amount, - memo, overviewURL: CONFIG.EMAIL_LINK_OVERVIEW, }) if (transactionLink) { diff --git a/backend/src/mailer/sendTransactionLinkRedeemed.test.ts b/backend/src/mailer/sendTransactionLinkRedeemed.test.ts new file mode 100644 index 000000000..b56ff40a1 --- /dev/null +++ b/backend/src/mailer/sendTransactionLinkRedeemed.test.ts @@ -0,0 +1,44 @@ +import { sendEMail } from './sendEMail' +import Decimal from 'decimal.js-light' +import { sendTransactionLinkRedeemedEmail } from './sendTransactionLinkRedeemed' + +jest.mock('./sendEMail', () => { + return { + __esModule: true, + sendEMail: jest.fn(), + } +}) + +describe('sendTransactionLinkRedeemedEmail', () => { + beforeEach(async () => { + await sendTransactionLinkRedeemedEmail({ + email: 'bibi@bloxberg.de', + senderFirstName: 'Peter', + senderLastName: 'Lustig', + recipientFirstName: 'Bibi', + recipientLastName: 'Bloxberg', + senderEmail: 'peter@lustig.de', + amount: new Decimal(42.0), + memo: 'Vielen Dank dass Du dabei bist', + overviewURL: 'http://localhost/overview', + }) + }) + + it('calls sendEMail', () => { + expect(sendEMail).toBeCalledWith({ + to: `Bibi Bloxberg `, + subject: 'Gradido-Link wurde eingelöst', + text: + expect.stringContaining('Hallo Bibi Bloxberg') && + expect.stringContaining( + 'Peter Lustig (peter@lustig.de) hat soeben deinen Link eingelöst.', + ) && + expect.stringContaining('Betrag: 42,00 GDD,') && + expect.stringContaining('Memo: Vielen Dank dass Du dabei bist') && + expect.stringContaining( + 'Details zur Transaktion findest du in deinem Gradido-Konto: http://localhost/overview', + ) && + expect.stringContaining('Bitte antworte nicht auf diese E-Mail!'), + }) + }) +}) diff --git a/backend/src/mailer/sendTransactionLinkRedeemed.ts b/backend/src/mailer/sendTransactionLinkRedeemed.ts new file mode 100644 index 000000000..a78f3b3c9 --- /dev/null +++ b/backend/src/mailer/sendTransactionLinkRedeemed.ts @@ -0,0 +1,28 @@ +import { backendLogger as logger } from '@/server/logger' +import Decimal from 'decimal.js-light' +import { sendEMail } from './sendEMail' +import { transactionLinkRedeemed } from './text/transactionLinkRedeemed' + +export const sendTransactionLinkRedeemedEmail = (data: { + email: string + senderFirstName: string + senderLastName: string + recipientFirstName: string + recipientLastName: string + senderEmail: string + amount: Decimal + memo: string + overviewURL: string +}): Promise => { + logger.info( + `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName}, + <${data.email}>, + subject=${transactionLinkRedeemed.de.subject}, + text=${transactionLinkRedeemed.de.text(data)}`, + ) + return sendEMail({ + to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, + subject: transactionLinkRedeemed.de.subject, + text: transactionLinkRedeemed.de.text(data), + }) +} diff --git a/backend/src/mailer/sendTransactionReceivedEmail.test.ts b/backend/src/mailer/sendTransactionReceivedEmail.test.ts index 75631cc7a..9f2ba9938 100644 --- a/backend/src/mailer/sendTransactionReceivedEmail.test.ts +++ b/backend/src/mailer/sendTransactionReceivedEmail.test.ts @@ -19,7 +19,6 @@ describe('sendTransactionReceivedEmail', () => { email: 'peter@lustig.de', senderEmail: 'bibi@bloxberg.de', amount: new Decimal(42.0), - memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', overviewURL: 'http://localhost/overview', }) }) @@ -33,7 +32,6 @@ describe('sendTransactionReceivedEmail', () => { expect.stringContaining('42,00 GDD') && expect.stringContaining('Bibi Bloxberg') && expect.stringContaining('(bibi@bloxberg.de)') && - expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!') && expect.stringContaining('http://localhost/overview'), }) }) diff --git a/backend/src/mailer/sendTransactionReceivedEmail.ts b/backend/src/mailer/sendTransactionReceivedEmail.ts index 55f63e37e..5e981659c 100644 --- a/backend/src/mailer/sendTransactionReceivedEmail.ts +++ b/backend/src/mailer/sendTransactionReceivedEmail.ts @@ -1,7 +1,7 @@ import { backendLogger as logger } from '@/server/logger' import Decimal from 'decimal.js-light' import { sendEMail } from './sendEMail' -import { transactionLinkRedeemed, transactionReceived } from './text/transactionReceived' +import { transactionReceived } from './text/transactionReceived' export const sendTransactionReceivedEmail = (data: { senderFirstName: string @@ -11,7 +11,6 @@ export const sendTransactionReceivedEmail = (data: { email: string senderEmail: string amount: Decimal - memo: string overviewURL: string }): Promise => { logger.info( @@ -26,27 +25,3 @@ export const sendTransactionReceivedEmail = (data: { text: transactionReceived.de.text(data), }) } - -export const sendTransactionLinkRedeemedEmail = (data: { - email: string - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - senderEmail: string - amount: Decimal - memo: string - overviewURL: string -}): Promise => { - logger.info( - `sendEmail(): to=${data.recipientFirstName} ${data.recipientLastName}, - <${data.email}>, - subject=${transactionLinkRedeemed.de.subject}, - text=${transactionLinkRedeemed.de.text(data)}`, - ) - return sendEMail({ - to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, - subject: transactionLinkRedeemed.de.subject, - text: transactionLinkRedeemed.de.text(data), - }) -} diff --git a/backend/src/mailer/text/transactionLinkRedeemed.ts b/backend/src/mailer/text/transactionLinkRedeemed.ts new file mode 100644 index 000000000..4d8e89cae --- /dev/null +++ b/backend/src/mailer/text/transactionLinkRedeemed.ts @@ -0,0 +1,33 @@ +import Decimal from 'decimal.js-light' + +export const transactionLinkRedeemed = { + de: { + subject: 'Gradido-Link wurde eingelöst', + text: (data: { + email: string + senderFirstName: string + senderLastName: string + recipientFirstName: string + recipientLastName: string + senderEmail: string + amount: Decimal + memo: string + overviewURL: string + }): string => + `Hallo ${data.recipientFirstName} ${data.recipientLastName} + + ${data.senderFirstName} ${data.senderLastName} (${ + data.senderEmail + }) hat soeben deinen Link eingelöst. + + Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD, + Memo: ${data.memo} + + Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} + + Bitte antworte nicht auf diese E-Mail! + + Mit freundlichen Grüßen, + dein Gradido-Team`, + }, +} diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts index 4f10f41c8..ba61ae680 100644 --- a/backend/src/mailer/text/transactionReceived.ts +++ b/backend/src/mailer/text/transactionReceived.ts @@ -11,7 +11,6 @@ export const transactionReceived = { email: string senderEmail: string amount: Decimal - memo: string overviewURL: string }): string => `Hallo ${data.recipientFirstName} ${data.recipientLastName} @@ -28,35 +27,3 @@ Mit freundlichen Grüßen, dein Gradido-Team`, }, } - -export const transactionLinkRedeemed = { - de: { - subject: 'Gradido link eingelösst', - text: (data: { - email: string - senderFirstName: string - senderLastName: string - recipientFirstName: string - recipientLastName: string - senderEmail: string - amount: Decimal - memo: string - overviewURL: string - }): string => - `Hallo ${data.recipientFirstName} ${data.recipientLastName} - -${data.senderFirstName} ${data.senderLastName} (${ - data.senderEmail - }) hat soeben deinen Link eingelöst. - -Betrag: ${data.amount.toFixed(2).replace('.', ',')} GDD, -Memo: ${data.memo} - -Details zur Transaktion findest du in deinem Gradido-Konto: ${data.overviewURL} - -Bitte antworte nicht auf diese E-Mail! - -Mit freundlichen Grüßen, -dein Gradido-Team`, - }, -} From 6202ca64ad809f888cae855723f421b3f121a70a Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 21 Sep 2022 19:50:27 +0200 Subject: [PATCH 80/92] test for register through transaction link done --- .../src/graphql/resolver/UserResolver.test.ts | 180 +++++++++++++----- 1 file changed, 128 insertions(+), 52 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 1103f8bf2..08256a11a 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -4,7 +4,15 @@ import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' +import { + createUser, + setPassword, + forgotPassword, + updateUserInfos, + createTransactionLink, + createContribution, + confirmContribution, +} from '@/seeds/graphql/mutations' import { login, logout, verifyLogin, queryOptIn, searchAdminUsers } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' @@ -15,17 +23,18 @@ import { sendAccountMultiRegistrationEmail } from '@/mailer/sendAccountMultiRegi import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' import { printTimeDuration, activationLink } from './UserResolver' import { contributionLinkFactory } from '@/seeds/factory/contributionLink' -// import { transactionLinkFactory } from '@/seeds/factory/transactionLink' +import { transactionLinkFactory } from '@/seeds/factory/transactionLink' import { ContributionLink } from '@model/ContributionLink' -// import { TransactionLink } from '@entity/TransactionLink' +import { TransactionLink } from '@entity/TransactionLink' import { EventProtocolType } from '@/event/EventProtocolType' import { EventProtocol } from '@entity/EventProtocol' import { logger } from '@test/testSetup' import { validate as validateUUID, version as versionUUID } from 'uuid' import { peterLustig } from '@/seeds/users/peter-lustig' -import { TransactionLink } from '@entity/TransactionLink' -import { transactionLinkFactory } from '@/seeds/factory/transactionLink' +import { creationFactory } from '@/seeds/factory/creation' +import { creations } from '@/seeds/creation' +import { bobBaumeister } from '@/seeds/users/bob-baumeister' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -250,40 +259,47 @@ describe('UserResolver', () => { }) describe('redeem codes', () => { - describe('contribution link', () => { - let link: ContributionLink - beforeAll(async () => { - // activate account of admin Peter Lustig - await mutate({ - mutation: setPassword, - variables: { code: emailOptIn, password: 'Aa12345_' }, - }) - - // make Peter Lustig Admin - const peter = await User.findOneOrFail({ id: user[0].id }) - peter.isAdmin = new Date() - await peter.save() - - // date statement - const actualDate = new Date() - const futureDate = new Date() // Create a future day from the executed day - futureDate.setDate(futureDate.getDate() + 1) - - // factory logs in as Peter Lustig - link = await contributionLinkFactory(testEnv, { - name: 'Dokumenta 2022', - memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', - amount: 200, - validFrom: actualDate, - validTo: futureDate, - }) - resetToken() - await mutate({ - mutation: createUser, - variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code }, - }) + let result: any + let link: ContributionLink + beforeAll(async () => { + // activate account of admin Peter Lustig + await mutate({ + mutation: setPassword, + variables: { code: emailOptIn, password: 'Aa12345_' }, }) + // make Peter Lustig Admin + const peter = await User.findOneOrFail({ id: user[0].id }) + peter.isAdmin = new Date() + await peter.save() + + // date statement + const actualDate = new Date() + const futureDate = new Date() // Create a future day from the executed day + futureDate.setDate(futureDate.getDate() + 1) + + // factory logs in as Peter Lustig + link = await contributionLinkFactory(testEnv, { + name: 'Dokumenta 2022', + memo: 'Vielen Dank für deinen Besuch bei der Dokumenta 2022', + amount: 100, + validFrom: actualDate, + validTo: futureDate, + }) + + resetToken() + + result = await mutate({ + mutation: createUser, + variables: { ...variables, email: 'ein@besucher.de', redeemCode: 'CL-' + link.code }, + }) + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('contribution link', () => { it('sets the contribution link id', async () => { await expect(User.findOne({ email: 'ein@besucher.de' })).resolves.toEqual( expect.objectContaining({ @@ -296,7 +312,7 @@ describe('UserResolver', () => { expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.ACTIVATE_ACCOUNT, - userId: expect.any(Number), // as it is randomly generated + userId: user[0].id, }), ) }) @@ -305,32 +321,92 @@ describe('UserResolver', () => { expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.REDEEM_REGISTER, - userId: expect.any(Number), // as it is randomly generated + userId: result.data.createUser.id, + contributionId: link.id, }), ) }) }) describe('transaction link', () => { + let contribution: any + let bob: any + let peter: any + let transactionLink: TransactionLink + let newUser: any + + const bobData = { + email: 'bob@baumeister.de', + password: 'Aa12345_', + publisherId: 1234, + } + + const peterData = { + email: 'peter@lustig.de', + password: 'Aa12345_', + publisherId: 1234, + } + beforeAll(async () => { - await transactionLinkFactory(testEnv, { - email: 'peter@lustig.de', - amount: 19.99, - memo: `Kein Trick, keine Zauberrei, - bei Gradidio sei dabei!`, + await userFactory(testEnv, bobBaumeister) + await query({ query: login, variables: bobData }) + + // create contribution as user bob + contribution = await mutate({ + mutation: createContribution, + variables: { amount: 1000, memo: 'testing', creationDate: new Date().toISOString() }, }) - const transactionLink = await TransactionLink.findOneOrFail() - resetToken() - await mutate({ - mutation: createUser, - variables: { ...variables, email: 'neuer@user.de', redeemCode: transactionLink.code }, + // login as admin + await query({ query: login, variables: peterData }) + + // confirm the contribution + contribution = await mutate({ + mutation: confirmContribution, + variables: { id: contribution.data.createContribution.id }, }) + + // login as user bob + bob = await query({ query: login, variables: bobData }) + + // create transaction link + await transactionLinkFactory(testEnv, { + email: 'bob@baumeister.de', + amount: 19.99, + memo: `testing transaction link`, + }) + + transactionLink = await TransactionLink.findOneOrFail() + + resetToken() + + // create new user using transaction link of bob + newUser = await mutate({ + mutation: createUser, + variables: { + ...variables, + email: 'which@ever.de', + redeemCode: transactionLink.code, + }, + }) + + console.log(await User.find()) + console.log(await EventProtocol.find()) }) - it('sets the referrer id to Peter Lustigs id', () => { - expect(User.findOne({ email: 'neuer@user.de' })).resolves.toEqual( - expect.objectContaining({ referrerId: user[0].id }), + it('sets the referrer id to bob baumeister id', () => { + expect(User.findOne({ email: 'which@ever.de' })).resolves.toEqual( + expect.objectContaining({ referrerId: bob.data.login.id }), + ) + }) + + // THIS ONE FAILS WITHOUT CONSOLE LOGS + it('stores the redeem register event in the database', () => { + expect(EventProtocol.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventProtocolType.REDEEM_REGISTER, + userId: newUser.data.createUser.id, + }), ) }) }) From c822580c1a2789cea653503a688e722c412c8028 Mon Sep 17 00:00:00 2001 From: joseji Date: Wed, 21 Sep 2022 19:53:57 +0200 Subject: [PATCH 81/92] removed unused variable --- 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 08256a11a..6f3fd2d1d 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -331,7 +331,6 @@ describe('UserResolver', () => { describe('transaction link', () => { let contribution: any let bob: any - let peter: any let transactionLink: TransactionLink let newUser: any From 5c784abe8a00e9222a59993901ca7084b513b4c8 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 22 Sep 2022 08:53:58 +0200 Subject: [PATCH 82/92] test client request time in request header --- frontend/src/plugins/apolloProvider.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/plugins/apolloProvider.test.js b/frontend/src/plugins/apolloProvider.test.js index aeb02243c..31e1a664b 100644 --- a/frontend/src/plugins/apolloProvider.test.js +++ b/frontend/src/plugins/apolloProvider.test.js @@ -98,6 +98,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: 'Bearer some-token', + clientRequestTime: expect.any(String), }, }) }) @@ -113,6 +114,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: '', + clientRequestTime: expect.any(String), }, }) }) From 91acb5f4b82edd2a5dd47920a1fb1a5939e1edef Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 22 Sep 2022 08:57:06 +0200 Subject: [PATCH 83/92] test client request time in request header --- admin/src/plugins/apolloProvider.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/admin/src/plugins/apolloProvider.test.js b/admin/src/plugins/apolloProvider.test.js index 75e415901..7889c3318 100644 --- a/admin/src/plugins/apolloProvider.test.js +++ b/admin/src/plugins/apolloProvider.test.js @@ -94,6 +94,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: 'Bearer some-token', + clientRequestTime: expect.any(String), }, }) }) @@ -109,6 +110,7 @@ describe('apolloProvider', () => { expect(setContextMock).toBeCalledWith({ headers: { Authorization: '', + clientRequestTime: expect.any(String), }, }) }) From f710fd9b4560ebff9bedaf38f1641f2b3d9b5fad Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 22 Sep 2022 11:19:32 +0200 Subject: [PATCH 84/92] fixed await for events redeem register --- backend/src/graphql/resolver/UserResolver.test.ts | 12 ++++-------- backend/src/graphql/resolver/UserResolver.ts | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 840e1ea4a..fd4fc6d09 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -389,20 +389,16 @@ describe('UserResolver', () => { redeemCode: transactionLink.code, }, }) - - console.log(await User.find()) - console.log(await EventProtocol.find()) }) - it('sets the referrer id to bob baumeister id', () => { - expect(User.findOne({ email: 'which@ever.de' })).resolves.toEqual( + it('sets the referrer id to bob baumeister id', async () => { + await expect(User.findOne({ email: 'which@ever.de' })).resolves.toEqual( expect.objectContaining({ referrerId: bob.data.login.id }), ) }) - // THIS ONE FAILS WITHOUT CONSOLE LOGS - it('stores the redeem register event in the database', () => { - expect(EventProtocol.find()).resolves.toContainEqual( + it('stores the redeem register event in the database', async () => { + await expect(EventProtocol.find()).resolves.toContainEqual( expect.objectContaining({ type: EventProtocolType.REDEEM_REGISTER, userId: newUser.data.createUser.id, diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index f2fd048fc..18cd8134b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -494,10 +494,10 @@ export class UserResolver { if (redeemCode) { eventRedeemRegister.userId = dbUser.id - eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) + await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) } else { eventRegister.userId = dbUser.id - eventProtocol.writeEvent(event.setEventRegister(eventRegister)) + await eventProtocol.writeEvent(event.setEventRegister(eventRegister)) } return new User(dbUser) From db21cf2e8d9e524d52a5c42f78b0b0ece09ed605 Mon Sep 17 00:00:00 2001 From: joseji Date: Thu, 22 Sep 2022 11:49:56 +0200 Subject: [PATCH 85/92] removed unused imports --- backend/src/graphql/resolver/UserResolver.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index fd4fc6d09..7ba74aff8 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -9,7 +9,6 @@ import { setPassword, forgotPassword, updateUserInfos, - createTransactionLink, createContribution, confirmContribution, } from '@/seeds/graphql/mutations' @@ -32,8 +31,6 @@ import { EventProtocol } from '@entity/EventProtocol' import { logger } from '@test/testSetup' import { validate as validateUUID, version as versionUUID } from 'uuid' import { peterLustig } from '@/seeds/users/peter-lustig' -import { creationFactory } from '@/seeds/factory/creation' -import { creations } from '@/seeds/creation' import { bobBaumeister } from '@/seeds/users/bob-baumeister' // import { klicktippSignIn } from '@/apis/KlicktippController' From de234cc7df28e4ba6795302b504cbe938fc1885e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Thu, 22 Sep 2022 23:29:51 +0200 Subject: [PATCH 86/92] rework PR remarks --- backend/.env.dist | 2 +- backend/.env.template | 1 - backend/src/config/index.ts | 3 +-- backend/src/server/createServer.ts | 6 +----- deployment/bare_metal/setup.md | 3 --- deployment/bare_metal/start.sh | 12 ++++++------ 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/backend/.env.dist b/backend/.env.dist index 06e0c3a64..648a2054c 100644 --- a/backend/.env.dist +++ b/backend/.env.dist @@ -38,7 +38,7 @@ LOGIN_SERVER_KEY=a51ef8ac7ef1abf162fb7a65261acd7a # EMail EMAIL=false EMAIL_TEST_MODUS=false -EMAIL_TEST_RECEIVER=test_team@gradido.net +EMAIL_TEST_RECEIVER=stage1@gradido.net EMAIL_USERNAME=gradido_email EMAIL_SENDER=info@gradido.net EMAIL_PASSWORD=xxx diff --git a/backend/.env.template b/backend/.env.template index bace78673..dddf845dc 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -5,7 +5,6 @@ JWT_SECRET=$JWT_SECRET JWT_EXPIRES_IN=$JWT_EXPIRES_IN GRAPHIQL=false GDT_API_URL=$GDT_API_URL -ENV_NAME=$ENV_NAME # Database DB_HOST=localhost diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 462620b14..edda4fbbb 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -28,7 +28,6 @@ const server = { JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GDT_API_URL: process.env.GDT_API_URL || 'https://gdt.gradido.net', - ENV_NAME: process.env.NODE_ENV || 'production', PRODUCTION: process.env.NODE_ENV === 'production' || false, } @@ -69,7 +68,7 @@ const loginServer = { const email = { EMAIL: process.env.EMAIL === 'true' || false, EMAIL_TEST_MODUS: process.env.EMAIL_TEST_MODUS === 'true' || 'false', - EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'test_team@gradido.net', + EMAIL_TEST_RECEIVER: process.env.EMAIL_TEST_RECEIVER || 'stage1@gradido.net', EMAIL_USERNAME: process.env.EMAIL_USERNAME || 'gradido_email', EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net', EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || 'xxx', diff --git a/backend/src/server/createServer.ts b/backend/src/server/createServer.ts index 3d89f7737..d1153cdb6 100644 --- a/backend/src/server/createServer.ts +++ b/backend/src/server/createServer.ts @@ -76,12 +76,8 @@ const createServer = async ( }) apollo.applyMiddleware({ app, path: '/' }) logger.info( - `running in GRADIDO_ENV_NAME=${CONFIG.ENV_NAME} as PRODUCTION=${CONFIG.PRODUCTION} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`, + `running with PRODUCTION=${CONFIG.PRODUCTION}, sending EMAIL enabled=${CONFIG.EMAIL} and EMAIL_TEST_MODUS=${CONFIG.EMAIL_TEST_MODUS} ...`, ) - if (CONFIG.PRODUCTION && CONFIG.EMAIL_TEST_MODUS === 'true') { - logger.error(`### RUNNING ENVIRONMENT Production IN EMAIL_TEST_MODE IS NOT ALLOWED ###`) - throw new Error(`### RUNNING ENVIRONMENT Production IN EMAIL_TEST_MODE IS NOT ALLOWED ###`) - } logger.debug('createServer...successful') return { apollo, app, con } } diff --git a/deployment/bare_metal/setup.md b/deployment/bare_metal/setup.md index 2f9a702e3..652a0a5ce 100644 --- a/deployment/bare_metal/setup.md +++ b/deployment/bare_metal/setup.md @@ -95,9 +95,6 @@ > cp .env.dist .env > nano .env >> Adjust values accordingly -# Define name of gradido environment -> the env variable GRADIDO_ENV_NAME have to be set during system installation manually to "development, stage1, stage2, stage3, production" -> export GRADIDO_ENV_NAME=development # Define cronjob to compensate yarn output in /tmp > yarn creates output in /tmp directory, which must be deleted regularly and will be done per cronjob > on stage1 a hourly job is necessary by setting the following job in the crontab for the gradido user diff --git a/deployment/bare_metal/start.sh b/deployment/bare_metal/start.sh index 8b0277fa7..95b89241f 100755 --- a/deployment/bare_metal/start.sh +++ b/deployment/bare_metal/start.sh @@ -120,8 +120,8 @@ yarn build if [ "$DEPLOY_SEED_DATA" = "true" ]; then yarn seed fi -# the env variable GRADIDO_ENV_NAME have to be set during system installation manually (development, stage1, stage2, stage3) if it should differ from production -export NODE_ENV="${GRADIDO_ENV_NAME:=production}" +# TODO maybe handle this differently? +export NODE_ENV=production pm2 delete gradido-backend pm2 start --name gradido-backend "yarn --cwd $PROJECT_ROOT/backend start" -l $GRADIDO_LOG_PATH/pm2.backend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save @@ -133,8 +133,8 @@ cd $PROJECT_ROOT/frontend unset NODE_ENV yarn install yarn build -# the env variable GRADIDO_ENV_NAME have to be set during system installation manually (development, stage1, stage2, stage3) if it should differ from production -export NODE_ENV="${GRADIDO_ENV_NAME:=production}" +# TODO maybe handle this differently? +export NODE_ENV=production pm2 delete gradido-frontend pm2 start --name gradido-frontend "yarn --cwd $PROJECT_ROOT/frontend start" -l $GRADIDO_LOG_PATH/pm2.frontend.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save @@ -146,8 +146,8 @@ cd $PROJECT_ROOT/admin unset NODE_ENV yarn install yarn build -# the env variable GRADIDO_ENV_NAME have to be set during system installation manually (development, stage1, stage2, stage3) if it should differ from production -export NODE_ENV="${GRADIDO_ENV_NAME:=production}" +# TODO maybe handle this differently? +export NODE_ENV=production pm2 delete gradido-admin pm2 start --name gradido-admin "yarn --cwd $PROJECT_ROOT/admin start" -l $GRADIDO_LOG_PATH/pm2.admin.$TODAY.log --log-date-format 'YYYY-MM-DD HH:mm:ss.SSS' pm2 save From 0b410fd5fd67c51266550edb86da4bf998118f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 23 Sep 2022 00:50:07 +0200 Subject: [PATCH 87/92] oel convert --- docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index f8fde0430..fe2f68a8d 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -90,7 +90,7 @@ services: networks: - internal-net - external-net - + ######################################################### ## NGINX ################################################ ######################################################### From 62446bd2f0218f484aa70ecabc4081a088c1796c Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Fri, 23 Sep 2022 00:56:36 +0200 Subject: [PATCH 88/92] Update backend/src/graphql/resolver/AdminResolver.test.ts Co-authored-by: Moriz Wahl --- backend/src/graphql/resolver/AdminResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/graphql/resolver/AdminResolver.test.ts b/backend/src/graphql/resolver/AdminResolver.test.ts index 2f72155de..b1b4e469e 100644 --- a/backend/src/graphql/resolver/AdminResolver.test.ts +++ b/backend/src/graphql/resolver/AdminResolver.test.ts @@ -1498,7 +1498,7 @@ describe('AdminResolver', () => { }) // In the futrue this should not throw anymore - it('and throws an error for the second confirmation', async () => { + it('throws an error for the second confirmation', async () => { const r1 = mutate({ mutation: confirmContribution, variables: { From b0781bce88066fc887578852964376d93426af4b Mon Sep 17 00:00:00 2001 From: clauspeterhuebner <86960882+clauspeterhuebner@users.noreply.github.com> Date: Fri, 23 Sep 2022 01:03:07 +0200 Subject: [PATCH 89/92] Update backend/src/graphql/resolver/UserResolver.test.ts Co-authored-by: Moriz Wahl --- backend/src/graphql/resolver/UserResolver.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 5fef81ef1..7b59cb134 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -214,8 +214,7 @@ describe('UserResolver', () => { mutation: createUser, variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' }, }) - await expect(User.find({ relations: ['emailContact'] })).resolves.toEqual( - expect.arrayContaining([ + await expect(User.find({ relations: ['emailContact'] })).resolves.toContain( expect.objectContaining({ emailContact: expect.objectContaining({ email: 'bibi@bloxberg.de', From e701f995536ee764d83ecbd0bb2f3ce27de8c03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 23 Sep 2022 01:11:37 +0200 Subject: [PATCH 90/92] rework PR comments --- backend/src/graphql/resolver/TransactionLinkResolver.ts | 2 +- backend/src/graphql/resolver/UserResolver.test.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/backend/src/graphql/resolver/TransactionLinkResolver.ts b/backend/src/graphql/resolver/TransactionLinkResolver.ts index d8be2d552..c9acbace3 100644 --- a/backend/src/graphql/resolver/TransactionLinkResolver.ts +++ b/backend/src/graphql/resolver/TransactionLinkResolver.ts @@ -285,7 +285,7 @@ export class TransactionLinkResolver { const transactionLink = await dbTransactionLink.findOneOrFail({ code }) const linkedUser = await dbUser.findOneOrFail( { id: transactionLink.userId }, - { relations: ['user'] }, + { relations: ['emailContact'] }, ) if (user.id === linkedUser.id) { diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 5fef81ef1..d62762288 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -701,12 +701,10 @@ bei Gradidio sei dabei!`, }) describe('queryOptIn', () => { - // let loginEmailOptIn: LoginEmailOptIn[] let emailContact: UserContact beforeAll(async () => { await userFactory(testEnv, bibiBloxberg) - // loginEmailOptIn = await LoginEmailOptIn.find() emailContact = await UserContact.findOneOrFail({ email: bibiBloxberg.email }) }) From ab1a4ac8d14a89a48e54bd6e403160a217998bdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 23 Sep 2022 18:24:13 +0200 Subject: [PATCH 91/92] solve tests --- .../src/graphql/resolver/UserResolver.test.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 722571baf..53dc392ba 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -233,12 +233,12 @@ describe('UserResolver', () => { mutation: createUser, variables: { ...variables, email: 'bibi@bloxberg.de', language: 'it' }, }) - await expect(User.find({ relations: ['emailContact'] })).resolves.toContain( + await expect( + UserContact.findOne({ email: 'bibi@bloxberg.de' }, { relations: ['user'] }), + ).resolves.toEqual( expect.objectContaining({ - emailContact: expect.objectContaining({ - email: 'bibi@bloxberg.de', - }), - language: 'de', + email: 'bibi@bloxberg.de', + user: expect.objectContaining({ language: 'de' }), }), ) }) @@ -401,8 +401,12 @@ describe('UserResolver', () => { }) it('sets the referrer id to bob baumeister id', async () => { - await expect(User.findOne({ email: 'which@ever.de' })).resolves.toEqual( - expect.objectContaining({ referrerId: bob.data.login.id }), + await expect( + UserContact.findOne({ email: 'which@ever.de' }, { relations: ['user'] }), + ).resolves.toEqual( + expect.objectContaining({ + user: expect.objectContaining({ referrerId: bob.data.login.id }), + }), ) }) @@ -577,6 +581,7 @@ describe('UserResolver', () => { describe('no users in database', () => { beforeAll(async () => { + jest.clearAllMocks() result = await query({ query: login, variables }) }) @@ -589,7 +594,9 @@ describe('UserResolver', () => { }) it('logs the error found', () => { - expect(logger.error).toBeCalledWith('User with email=bibi@bloxberg.de does not exist') + expect(logger.error).toBeCalledWith( + 'UserContact with email=bibi@bloxberg.de does not exists', + ) }) }) From 792d8a8d53ccb1836104985af413f5ce58470995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus-Peter=20H=C3=BCbner?= Date: Fri, 23 Sep 2022 18:47:07 +0200 Subject: [PATCH 92/92] eol convert --- docker-compose.test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 036149b7b..79ee46906 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -59,3 +59,4 @@ networks: volumes: db_test_vol: +