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.