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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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/57] 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 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 49/57] 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 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 50/57] 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 51/57] 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 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 52/57] 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 53/57] 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 54/57] 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 55/57] 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 56/57] 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 57/57] 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: +