From 0c4082d654faed11bc4440d83c5fdc9ec1052452 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 14 Nov 2024 14:57:45 +0100 Subject: [PATCH 01/12] Feat/User email verification --- ...1653657-addUserEmailVerificationColumns.ts | 21 ++++++++++ .../notifications/MockNotificationAdapter.ts | 8 ++++ .../NotificationAdapterInterface.ts | 5 +++ .../NotificationCenterAdapter.ts | 22 +++++++++++ src/analytics/analytics.ts | 2 + src/entities/user.ts | 8 ++++ src/resolvers/userResolver.ts | 38 +++++++++++++++++++ src/utils/errorMessages.ts | 1 + src/utils/locales/en.json | 5 ++- src/utils/locales/es.json | 3 +- src/utils/utils.ts | 16 ++++++++ 11 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 migration/1731071653657-addUserEmailVerificationColumns.ts diff --git a/migration/1731071653657-addUserEmailVerificationColumns.ts b/migration/1731071653657-addUserEmailVerificationColumns.ts new file mode 100644 index 000000000..1127cc0a9 --- /dev/null +++ b/migration/1731071653657-addUserEmailVerificationColumns.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserEmailVerificationColumns1731071653657 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + queryRunner.query(` + ALTER TABLE "user" + ADD COLUMN IF NOT EXISTS "emailVerificationCode" VARCHAR, + ADD COLUMN IF NOT EXISTS "isEmailVerified" BOOLEAN DEFAULT false; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + queryRunner.query(` + ALTER TABLE "user" + DROP COLUMN IF EXISTS "emailVerificationCode", + DROP COLUMN IF EXISTS "isEmailVerified"; + `); + } +} diff --git a/src/adapters/notifications/MockNotificationAdapter.ts b/src/adapters/notifications/MockNotificationAdapter.ts index e47a3825c..0b4727974 100644 --- a/src/adapters/notifications/MockNotificationAdapter.ts +++ b/src/adapters/notifications/MockNotificationAdapter.ts @@ -35,6 +35,14 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { return Promise.resolve(undefined); } + sendUserEmailConfirmationCodeFlow(params: { email: string }): Promise { + logger.debug( + 'MockNotificationAdapter sendUserEmailConfirmationCodeFlow', + params, + ); + return Promise.resolve(undefined); + } + userSuperTokensCritical(): Promise { return Promise.resolve(undefined); } diff --git a/src/adapters/notifications/NotificationAdapterInterface.ts b/src/adapters/notifications/NotificationAdapterInterface.ts index 7e19aaacb..ef09758cf 100644 --- a/src/adapters/notifications/NotificationAdapterInterface.ts +++ b/src/adapters/notifications/NotificationAdapterInterface.ts @@ -72,6 +72,11 @@ export interface NotificationAdapterInterface { networkName: string; }): Promise; + sendUserEmailConfirmationCodeFlow(params: { + email: string; + user: User; + }): Promise; + projectVerified(params: { project: Project }): Promise; projectBoosted(params: { projectId: number; userId: number }): Promise; projectBoostedBatch(params: { diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index 1cc873ba6..5eb4c9661 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -94,6 +94,28 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { } } + async sendUserEmailConfirmationCodeFlow(params: { + email: string; + user: User; + }): Promise { + const { email, user } = params; + try { + await callSendNotification({ + eventName: + NOTIFICATIONS_EVENT_NAMES.SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + segment: { + payload: { + email, + verificationCode: user.emailVerificationCode, + userId: user.id, + }, + }, + }); + } catch (e) { + logger.error('sendUserEmailConfirmationCodeFlow >> error', e); + } + } + async userSuperTokensCritical(params: { user: User; eventName: UserStreamBalanceWarning; diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 44b40ffc4..798436b64 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -49,4 +49,6 @@ export enum NOTIFICATIONS_EVENT_NAMES { SUBSCRIBE_ONBOARDING = 'Subscribe onboarding', CREATE_ORTTO_PROFILE = 'Create Ortto profile', SEND_EMAIL_CONFIRMATION = 'Send email confirmation', + + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW = 'Send email confirmation code flow', } diff --git a/src/entities/user.ts b/src/entities/user.ts index 12ca19950..da7d6fac1 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -34,6 +34,7 @@ export const publicSelectionFields = [ 'user.totalReceived', 'user.passportScore', 'user.passportStamps', + 'user.isEmailVerified', ]; export enum UserRole { @@ -195,6 +196,13 @@ export class User extends BaseEntity { @Field(_type => Float, { nullable: true }) activeQFMBDScore?: number; + @Field(_type => Boolean, { nullable: true }) + @Column('bool', { default: false }) + isEmailVerified: boolean; + + @Column('varchar', { nullable: true, default: null }) + emailVerificationCode?: string | null; + @Field(_type => Int, { nullable: true }) async donationsCount() { // Count for non-recurring donations diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 487f9ac28..5eee77bb7 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -30,6 +30,8 @@ import { isWalletAddressInPurpleList } from '../repositories/projectAddressRepos import { addressHasDonated } from '../repositories/donationRepository'; import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; import { retrieveActiveQfRoundUserMBDScore } from '../repositories/qfRoundRepository'; +import { getLoggedInUser } from '../services/authorizationServices'; +import { generateRandomNumericCode } from '../utils/utils'; @ObjectType() class UserRelatedAddressResponse { @@ -230,4 +232,40 @@ export class UserResolver { return true; } + + @Mutation(_returns => String) + async sendUserEmailConfirmationCodeFlow( + @Arg('email') email: string, + @Ctx() ctx: ApolloContext, + ): Promise { + const user = await getLoggedInUser(ctx); + + // Check if email aready veriffied + if (user.isEmailVerified) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), + ); + } + + // Check do we have an email already in the database + const isEmailAlreadyUsed = await User.findOne({ + where: { email: email }, + }); + + if (isEmailAlreadyUsed && isEmailAlreadyUsed.id !== user.id) { + return 'EMAIL_EXIST'; + } + + // Send verification code + const code = generateRandomNumericCode(5).toString(); + + user.emailVerificationCode = code; + + await getNotificationAdapter().sendUserEmailConfirmationCodeFlow({ + email: email, + user: user, + }); + + return 'VERIFICATION_SENT'; + } } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index a6584da2a..0fc4b1ee7 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -379,4 +379,5 @@ export const translationErrorMessagesKeys = { DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED: 'DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED', QR_CODE_DATA_URL_REQUIRED: 'QR_CODE_DATA_URL_REQUIRED', + USER_EMAIL_ALREADY_VERIFIED: 'USER_EMAIL_ALREADY_VERIFIED', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 70645329d..1647cff6a 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -117,5 +117,6 @@ "TX_NOT_FOUND": "TX_NOT_FOUND", "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION": "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION", "Project does not accept recurring donation": "Project does not accept recurring donation", - "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED" -} \ No newline at end of file + "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED", + "USER_EMAIL_ALREADY_VERIFIED": "User email already verified" +} diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index 7f8d184f5..ca78b3b4e 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -106,5 +106,6 @@ "PROJECT_UPDATE_CONTENT_LENGTH_SIZE_EXCEEDED": "El contenido es demasiado largo", "DRAFT_DONATION_DISABLED": "El borrador de donación está deshabilitado", "EVM_SUPPORT_ONLY": "Solo se admite EVM", - "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar" + "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar", + "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado" } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d50f467e9..52a3e34dc 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -466,3 +466,19 @@ export const isSocialMediaEqual = ( .sort(), ); }; + +/** + * Generates a random numeric code with the specified number of digits. + * + * @param {number} digits - The number of digits for the generated code. Defaults to 6 if not provided. + * @returns {number} A random numeric code with the specified number of digits. + * + * Example: + * generateRandomNumericCode(4) // Returns a 4-digit number, e.g., 3741 + * generateRandomNumericCode(8) // Returns an 8-digit number, e.g., 29384756 + */ +export const generateRandomNumericCode = (digits: number = 6): number => { + const min = Math.pow(10, digits - 1); + const max = Math.pow(10, digits) - 1; + return Math.floor(min + Math.random() * (max - min + 1)); +}; From d8897fac3feb5163d2b68c3284c211b941c25a82 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Mon, 18 Nov 2024 14:59:48 +0100 Subject: [PATCH 02/12] added confirmation flow for use inputed code --- src/resolvers/userResolver.ts | 99 ++++++++++++++++++++++++++++++++++- src/utils/errorMessages.ts | 2 + src/utils/locales/en.json | 4 +- src/utils/locales/es.json | 4 +- 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 5eee77bb7..1f931f5f0 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -233,6 +233,33 @@ export class UserResolver { return true; } + /** + * Mutation to handle the process of sending a user email confirmation code. + * + * This function performs the following steps: + * 1. **Retrieve Logged-In User**: Fetches the currently logged-in user using the context (`ctx`). + * 2. **Check Email Verification Status**: + * - If the user's email is already verified, it throws an error with an appropriate message. + * 3. **Check for Email Usage**: + * - Verifies if the provided email is already in use by another user in the database. + * - If the email exists and belongs to a different user, it returns `'EMAIL_EXIST'`. + * 4. **Generate Verification Code**: + * - Creates a random 5-digit numeric code for email verification. + * - Updates the logged-in user's email verification code and email in the database. + * 5. **Send Verification Code**: + * - Uses the notification adapter to send the generated verification code to the provided email. + * 6. **Save User Record**: + * - Saves the updated user information (email and verification code) to the database. + * 7. **Return Status**: + * - If the verification code is successfully sent, it returns `'VERIFICATION_SENT'`. + * + * @param {string} email - The email address to verify. + * @param {ApolloContext} ctx - The GraphQL context containing user and other relevant information. + * @returns {Promise} - A status string indicating the result of the operation: + * - `'EMAIL_EXIST'`: The email is already used by another user. + * - `'VERIFICATION_SENT'`: The verification code has been sent successfully. + * @throws {Error} - If the user's email is already verified. + */ @Mutation(_returns => String) async sendUserEmailConfirmationCodeFlow( @Arg('email') email: string, @@ -240,7 +267,7 @@ export class UserResolver { ): Promise { const user = await getLoggedInUser(ctx); - // Check if email aready veriffied + // Check if email aready verified if (user.isEmailVerified) { throw new Error( i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), @@ -260,12 +287,82 @@ export class UserResolver { const code = generateRandomNumericCode(5).toString(); user.emailVerificationCode = code; + user.email = email; await getNotificationAdapter().sendUserEmailConfirmationCodeFlow({ email: email, user: user, }); + await user.save(); + return 'VERIFICATION_SENT'; } + + /** + * Mutation to handle the user confirmation code verification process. + * + * This function performs the following steps: + * 1. **Retrieve Logged-In User**: Fetches the currently logged-in user using the provided context (`ctx`). + * 2. **Check Email Verification Status**: + * - If the user's email is already verified, an error is thrown with an appropriate message. + * 3. **Verify Email Verification Code Presence**: + * - Checks if the user has a stored email verification code in the database. + * - If no code exists, an error is thrown indicating that the code was not found. + * 4. **Validate the Verification Code**: + * - Compares the provided `verifyCode` with the user's stored email verification code. + * - If the codes do not match, an error is thrown indicating the mismatch. + * 5. **Mark Email as Verified**: + * - If the verification code matches, the user's `emailVerificationCode` is cleared (set to `null`), + * and the `isEmailVerified` flag is set to `true`. + * 6. **Save Updated User Data**: + * - The updated user record (email verified status) is saved to the database. + * 7. **Return Status**: + * - Returns `'VERIFICATION_SUCCESS'` to indicate the email verification was completed successfully. + * + * @param {string} verifyCode - The verification code submitted by the user for validation. + * @param {ApolloContext} ctx - The GraphQL context containing the logged-in user's information. + * @returns {Promise} - A status string indicating the result of the verification process: + * - `'VERIFICATION_SUCCESS'`: The email has been successfully verified. + * @throws {Error} - If: + * - The user's email is already verified. + * - No verification code is found in the database for the user. + * - The provided verification code does not match the stored code. + */ + @Mutation(_returns => String) + async sendUserConfirmationCodeFlow( + @Arg('verifyCode') verifyCode: string, + @Ctx() ctx: ApolloContext, + ): Promise { + const user = await getLoggedInUser(ctx); + + // Check if email aready verified + if (user.isEmailVerified) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), + ); + } + + // Check do we have an email verification code inside database + if (!user.emailVerificationCode) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_CODE_NOT_FOUND), + ); + } + + // Check if code matches + if (verifyCode !== user.emailVerificationCode) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_CODE_NOT_MATCH), + ); + } + + // Save Updated User Data + user.emailVerificationCode = null; + user.isEmailVerified = true; + + await user.save(); + + return 'VERIFICATION_SUCCESS'; + } } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 0fc4b1ee7..fcc19882a 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -380,4 +380,6 @@ export const translationErrorMessagesKeys = { 'DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED', QR_CODE_DATA_URL_REQUIRED: 'QR_CODE_DATA_URL_REQUIRED', USER_EMAIL_ALREADY_VERIFIED: 'USER_EMAIL_ALREADY_VERIFIED', + USER_EMAIL_CODE_NOT_FOUND: 'USER_EMAIL_CODE_NOT_FOUND', + USER_EMAIL_CODE_NOT_MATCH: 'USER_EMAIL_CODE_NOT_MATCH', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 1647cff6a..108888ae4 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -118,5 +118,7 @@ "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION": "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION", "Project does not accept recurring donation": "Project does not accept recurring donation", "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED", - "USER_EMAIL_ALREADY_VERIFIED": "User email already verified" + "USER_EMAIL_ALREADY_VERIFIED": "User email already verified", + "USER_EMAIL_CODE_NOT_FOUND": "User email verification code not found", + "USER_EMAIL_CODE_NOT_MATCH": "User email verification code not match" } diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index ca78b3b4e..2ce294b8d 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -107,5 +107,7 @@ "DRAFT_DONATION_DISABLED": "El borrador de donación está deshabilitado", "EVM_SUPPORT_ONLY": "Solo se admite EVM", "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar", - "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado" + "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado", + "USER_EMAIL_CODE_NOT_FOUND": "Código de verificación de correo electrónico de usuario no encontrado", + "USER_EMAIL_CODE_NOT_MATCH": "El código de verificación del correo electrónico del usuario no coincide" } From a7b30f65cd89039b34d0036bc4de701bf70953db Mon Sep 17 00:00:00 2001 From: kkatusic Date: Tue, 19 Nov 2024 14:32:05 +0100 Subject: [PATCH 03/12] added restriction for project and solved verification process --- src/resolvers/projectResolver.ts | 11 +++++++++++ src/resolvers/userResolver.ts | 17 ++++++++++++++--- src/services/authorizationServices.ts | 2 ++ src/utils/errorMessages.ts | 1 + src/utils/locales/en.json | 3 ++- src/utils/locales/es.json | 3 ++- 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 74915f8ac..ded592b28 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1079,6 +1079,12 @@ export class ProjectResolver { throw new Error( i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); + + // Check if user email is verified + if (!user.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const { image } = newProjectData; // const project = await Project.findOne({ id: projectId }); @@ -1362,6 +1368,11 @@ export class ProjectResolver { const user = await getLoggedInUser(ctx); const { image, description } = projectInput; + // Check if user email is verified + if (!user.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const qualityScore = getQualityScore(description, Boolean(image), 0); if (!projectInput.categories) { diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 1f931f5f0..f8f0f94fa 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -143,6 +143,7 @@ export class UserResolver { i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); const dbUser = await findUserById(user.userId); + if (!dbUser) { return false; } @@ -172,6 +173,14 @@ export class UserResolver { if (location !== undefined) { dbUser.location = location; } + // Check if user email is verified + if (!dbUser.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + // Check if old email is verified and user entered new one + if (dbUser.isEmailVerified && email !== dbUser.email) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } if (email !== undefined) { // User can unset his email by putting empty string if (!validateEmail(email)) { @@ -268,7 +277,7 @@ export class UserResolver { const user = await getLoggedInUser(ctx); // Check if email aready verified - if (user.isEmailVerified) { + if (user.isEmailVerified && user.email === email) { throw new Error( i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), ); @@ -287,7 +296,6 @@ export class UserResolver { const code = generateRandomNumericCode(5).toString(); user.emailVerificationCode = code; - user.email = email; await getNotificationAdapter().sendUserEmailConfirmationCodeFlow({ email: email, @@ -320,6 +328,7 @@ export class UserResolver { * 7. **Return Status**: * - Returns `'VERIFICATION_SUCCESS'` to indicate the email verification was completed successfully. * + * @param {string} email - The email address associated with the user's account. * @param {string} verifyCode - The verification code submitted by the user for validation. * @param {ApolloContext} ctx - The GraphQL context containing the logged-in user's information. * @returns {Promise} - A status string indicating the result of the verification process: @@ -331,13 +340,14 @@ export class UserResolver { */ @Mutation(_returns => String) async sendUserConfirmationCodeFlow( + @Arg('email') email: string, @Arg('verifyCode') verifyCode: string, @Ctx() ctx: ApolloContext, ): Promise { const user = await getLoggedInUser(ctx); // Check if email aready verified - if (user.isEmailVerified) { + if (user.isEmailVerified && user.email === email) { throw new Error( i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), ); @@ -360,6 +370,7 @@ export class UserResolver { // Save Updated User Data user.emailVerificationCode = null; user.isEmailVerified = true; + user.email = email; await user.save(); diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index bc0221f1c..b0e615256 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -44,6 +44,7 @@ export interface JwtVerifiedUser { firstName?: string; lastName?: string; walletAddress?: string; + isEmailVerified?: boolean; userId: number; token: string; } @@ -119,6 +120,7 @@ export const validateAuthMicroserviceJwt = async ( name: user?.name, walletAddress: user?.walletAddress, userId: user!.id, + isEmailVerified: user?.isEmailVerified, token, }; } catch (e) { diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index fcc19882a..2a3c2ac1d 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -382,4 +382,5 @@ export const translationErrorMessagesKeys = { USER_EMAIL_ALREADY_VERIFIED: 'USER_EMAIL_ALREADY_VERIFIED', USER_EMAIL_CODE_NOT_FOUND: 'USER_EMAIL_CODE_NOT_FOUND', USER_EMAIL_CODE_NOT_MATCH: 'USER_EMAIL_CODE_NOT_MATCH', + EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 108888ae4..d482bb50a 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -120,5 +120,6 @@ "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED", "USER_EMAIL_ALREADY_VERIFIED": "User email already verified", "USER_EMAIL_CODE_NOT_FOUND": "User email verification code not found", - "USER_EMAIL_CODE_NOT_MATCH": "User email verification code not match" + "USER_EMAIL_CODE_NOT_MATCH": "User email verification code not match", + "EMAIL_NOT_VERIFIED": "Email not verified" } diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index 2ce294b8d..80aa754ad 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -109,5 +109,6 @@ "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar", "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado", "USER_EMAIL_CODE_NOT_FOUND": "Código de verificación de correo electrónico de usuario no encontrado", - "USER_EMAIL_CODE_NOT_MATCH": "El código de verificación del correo electrónico del usuario no coincide" + "USER_EMAIL_CODE_NOT_MATCH": "El código de verificación del correo electrónico del usuario no coincide", + "EMAIL_NOT_VERIFIED": "Correo electrónico no verificado" } From a006f8aea4e4dc62ee614c8a95e339cd63c6e689 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 13:23:22 +0100 Subject: [PATCH 04/12] added tests for verification and bypassing first user update --- config/test.env | 16 +- src/resolvers/userResolver.test.ts | 336 ++++++++++++++++++++++++++++- src/resolvers/userResolver.ts | 19 +- src/utils/errorMessages.ts | 4 + test/graphqlQueries.ts | 2 + 5 files changed, 367 insertions(+), 10 deletions(-) diff --git a/config/test.env b/config/test.env index 8a3d288fa..bb42d5c5f 100644 --- a/config/test.env +++ b/config/test.env @@ -2,12 +2,20 @@ JWT_SECRET=000000000000000000000000000000000000000000000000000000000000000000000 MAILER_JWT_SECRET=0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 JWT_MAX_AGE=7d +#TYPEORM_DATABASE_TYPE=postgres +#TYPEORM_DATABASE_NAME=givethio +#TYPEORM_DATABASE_USER=postgres +#TYPEORM_DATABASE_PASSWORD=postgres +#TYPEORM_DATABASE_HOST=localhost +#TYPEORM_DATABASE_PORT=5443 + + TYPEORM_DATABASE_TYPE=postgres -TYPEORM_DATABASE_NAME=givethio +TYPEORM_DATABASE_NAME=staging-givethio TYPEORM_DATABASE_USER=postgres TYPEORM_DATABASE_PASSWORD=postgres -TYPEORM_DATABASE_HOST=localhost -TYPEORM_DATABASE_PORT=5443 +TYPEORM_DATABASE_HOST=127.0.0.1 +TYPEORM_DATABASE_PORT=5442 TYPEORM_LOGGING=all DROP_DATABASE=true @@ -257,4 +265,4 @@ STELLAR_HORIZON_API_URL=https://horizon.stellar.org STELLAR_SCAN_API_URL=https://stellar.expert/explorer/public ENDAOMENT_ADMIN_WALLET_ADDRESS=0xfE3524e04E4e564F9935D34bB5e80c5CaB07F5b4 -SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION=0 0 */3 * * \ No newline at end of file +SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION=0 0 */3 * * diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index d996747f9..fa76bea9d 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -33,6 +33,7 @@ import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; describe('updateUser() test cases', updateUserTestCases); describe('userByAddress() test cases', userByAddressTestCases); describe('refreshUserScores() test cases', refreshUserScoresTestCases); +describe('userEmailVerification() test cases', userEmailVerification); // TODO I think we can delete addUserVerification query // describe('addUserVerification() test cases', addUserVerificationTestCases); function refreshUserScoresTestCases() { @@ -615,6 +616,7 @@ function updateUserTestCases() { email: 'giveth@gievth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -653,6 +655,7 @@ function updateUserTestCases() { email: 'giveth@gievth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -709,7 +712,7 @@ function updateUserTestCases() { errorMessages.BOTH_FIRST_NAME_AND_LAST_NAME_CANT_BE_EMPTY, ); }); - it('should fail when email is invalid', async () => { + it('should fail when email is invalid first case', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { @@ -717,6 +720,7 @@ function updateUserTestCases() { email: 'giveth', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -733,7 +737,7 @@ function updateUserTestCases() { assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); }); - it('should fail when email is invalid', async () => { + it('should fail when email is invalid second case', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { @@ -741,6 +745,7 @@ function updateUserTestCases() { email: 'giveth @ giveth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -766,6 +771,7 @@ function updateUserTestCases() { email: 'giveth @ giveth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -794,6 +800,7 @@ function updateUserTestCases() { email: 'giveth @ giveth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -826,6 +833,7 @@ function updateUserTestCases() { avatar: 'pinata address', url: 'website url', lastName: new Date().getTime().toString(), + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -865,6 +873,7 @@ function updateUserTestCases() { avatar: 'pinata address', url: 'website url', firstName: new Date().getTime().toString(), + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -900,6 +909,7 @@ function updateUserTestCases() { lastName: 'test lastName', avatar: '', url: '', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -925,3 +935,325 @@ function updateUserTestCases() { assert.equal(updatedUser?.url, updateUserData.url); }); } + +function userEmailVerification() { + describe('userEmailVerification() test cases', () => { + it('should send a verification code if the email is valid and not used by another user', async () => { + // Create a user + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; // Ensure the email is not verified + await user.save(); + + // Update user email to match the expected test email + const newEmail = `newemail-${generateRandomEtheriumAddress()}@giveth.io`; + user.email = newEmail; // Update the email + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: newEmail }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the response + assert.equal( + result.data.data.sendUserEmailConfirmationCodeFlow, + 'VERIFICATION_SENT', + ); + + // Assert the user is updated in the database + const updatedUser = await User.findOne({ where: { id: user.id } }); + assert.equal(updatedUser?.email, newEmail); + assert.isNotNull(updatedUser?.emailVerificationCode); + }); + + it('should fail if the email is invalid', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: 'invalid-email' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); + }); + + it('should fail if the email is already verified', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = true; // Simulate an already verified email + user.email = 'already-verified@giveth.io'; + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: 'already-verified@giveth.io' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_ALREADY_VERIFIED, + ); + }); + + it('should return EMAIL_EXIST if the email is already used by another user', async () => { + const existingUser = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + existingUser.email = 'existing-user@giveth.io'; + existingUser.isEmailVerified = true; + await existingUser.save(); + + const newUser = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const accessToken = await generateTestAccessToken(newUser.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: 'existing-user@giveth.io' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the response + assert.equal( + result.data.data.sendUserEmailConfirmationCodeFlow, + 'EMAIL_EXIST', + ); + }); + }); + + describe('sendUserConfirmationCodeFlow() test cases', () => { + it('should successfully verify the email when provided with valid inputs', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; // Ensure email is not verified + user.emailVerificationCode = '12345'; // Set a valid verification code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = `verified-${generateRandomEtheriumAddress()}@giveth.io`; + const verifyCode = '12345'; + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the response + assert.equal( + result.data.data.sendUserConfirmationCodeFlow, + 'VERIFICATION_SUCCESS', + ); + + // Verify the database state + const updatedUser = await User.findOne({ where: { id: user.id } }); + assert.isTrue(updatedUser?.isEmailVerified); + assert.isNull(updatedUser?.emailVerificationCode); + assert.equal(updatedUser?.email, email); + }); + + it('should fail if the email format is invalid', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; + user.emailVerificationCode = '12345'; + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = 'invalid-email'; + const verifyCode = '12345'; + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); + }); + + it('should fail if the email is already verified', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = true; + user.email = 'already-verified@giveth.io'; + user.emailVerificationCode = null; // No verification code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { + email: 'already-verified@giveth.io', + verifyCode: '12345', + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_ALREADY_VERIFIED, + ); + }); + + it('should fail if no verification code is found', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; + user.emailVerificationCode = null; // No verification code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = `missing-code-${generateRandomEtheriumAddress()}@giveth.io`; + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode: '12345' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_CODE_NOT_FOUND, + ); + }); + + it('should fail if the verification code does not match', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; + user.emailVerificationCode = '54321'; // Incorrect code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = `mismatch-${generateRandomEtheriumAddress()}@giveth.io`; + const verifyCode = '12345'; // Incorrect code + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_CODE_NOT_MATCH, + ); + }); + }); +} diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index f8f0f94fa..66ad07ae3 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -136,6 +136,7 @@ export class UserResolver { @Arg('url', { nullable: true }) url: string, @Arg('avatar', { nullable: true }) avatar: string, @Arg('newUser', { nullable: true }) newUser: boolean, + @Arg('isFirstUpdate', { nullable: true }) isFirstUpdate: boolean, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user) @@ -173,12 +174,12 @@ export class UserResolver { if (location !== undefined) { dbUser.location = location; } - // Check if user email is verified - if (!dbUser.isEmailVerified) { + // Check if user email is verified and it's not the first update + if (!dbUser.isEmailVerified && !isFirstUpdate) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } - // Check if old email is verified and user entered new one - if (dbUser.isEmailVerified && email !== dbUser.email) { + // Check if old email is verified and user entered new one and it's not the first update + if (dbUser.isEmailVerified && email !== dbUser.email && !isFirstUpdate) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } if (email !== undefined) { @@ -276,6 +277,11 @@ export class UserResolver { ): Promise { const user = await getLoggedInUser(ctx); + // Check is mail valid + if (!validateEmail(email)) { + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_EMAIL)); + } + // Check if email aready verified if (user.isEmailVerified && user.email === email) { throw new Error( @@ -346,6 +352,11 @@ export class UserResolver { ): Promise { const user = await getLoggedInUser(ctx); + // Check is mail valid + if (!validateEmail(email)) { + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_EMAIL)); + } + // Check if email aready verified if (user.isEmailVerified && user.email === email) { throw new Error( diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 2a3c2ac1d..a1a304cc2 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -205,6 +205,10 @@ export const errorMessages = { DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED: 'Draft donation cannot be marked as failed', QR_CODE_DATA_URL_REQUIRED: 'QR code data URL is required', + USER_EMAIL_ALREADY_VERIFIED: 'User email already verified', + USER_EMAIL_CODE_NOT_FOUND: 'User email verification code not found', + USER_EMAIL_CODE_NOT_MATCH: 'User email verification code not match', + EMAIL_NOT_VERIFIED: 'Email not verified', }; export const translationErrorMessagesKeys = { diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 4f6dd6ca3..af25f7208 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1336,6 +1336,7 @@ export const updateUser = ` $firstName: String $avatar: String $newUser: Boolean + $isFirstUpdate: Boolean ) { updateUser( url: $url @@ -1345,6 +1346,7 @@ export const updateUser = ` lastName: $lastName avatar: $avatar newUser: $newUser + isFirstUpdate: $isFirstUpdate ) } `; From c432106f80d6f4c38f17651df89951147e5db06a Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 15:19:33 +0100 Subject: [PATCH 05/12] fixing project resolver test --- test/testUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/testUtils.ts b/test/testUtils.ts index 9cd3c21f6..0b4e9f7be 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -391,6 +391,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 1, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, SECOND_USER: { name: 'secondUser', @@ -400,6 +401,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 2, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, THIRD_USER: { name: 'thirdUser', @@ -409,6 +411,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 3, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, ADMIN_USER: { name: 'adminUser', @@ -418,6 +421,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 4, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, PROJECT_OWNER_USER: { name: 'project owner user', @@ -426,6 +430,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 5, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, FIRST_PROJECT: { ...createProjectData(), From f6497638f7217bacd7dcd0c09f7f6b766e180125 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 19:02:33 +0100 Subject: [PATCH 06/12] switch back variables --- config/test.env | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/config/test.env b/config/test.env index bb42d5c5f..6fb097ca6 100644 --- a/config/test.env +++ b/config/test.env @@ -2,20 +2,12 @@ JWT_SECRET=000000000000000000000000000000000000000000000000000000000000000000000 MAILER_JWT_SECRET=0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 JWT_MAX_AGE=7d -#TYPEORM_DATABASE_TYPE=postgres -#TYPEORM_DATABASE_NAME=givethio -#TYPEORM_DATABASE_USER=postgres -#TYPEORM_DATABASE_PASSWORD=postgres -#TYPEORM_DATABASE_HOST=localhost -#TYPEORM_DATABASE_PORT=5443 - - TYPEORM_DATABASE_TYPE=postgres -TYPEORM_DATABASE_NAME=staging-givethio +TYPEORM_DATABASE_NAME=givethio TYPEORM_DATABASE_USER=postgres TYPEORM_DATABASE_PASSWORD=postgres -TYPEORM_DATABASE_HOST=127.0.0.1 -TYPEORM_DATABASE_PORT=5442 +TYPEORM_DATABASE_HOST=localhost +TYPEORM_DATABASE_PORT=5443 TYPEORM_LOGGING=all DROP_DATABASE=true From f047276680d9e23c90a2e34526a7bb72c4feeabd Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 21:59:24 +0100 Subject: [PATCH 07/12] update jwt token with isEmailVerified test need this options --- src/services/authorizationServices.ts | 1 + test/graphqlQueries.ts | 1 + test/testUtils.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index b0e615256..43e995b3c 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -82,6 +82,7 @@ export const validateImpactGraphJwt = async ( lastName: decodedJwt?.lastName, walletAddress: decodedJwt?.walletAddress, userId: decodedJwt?.userId, + isEmailVerified: decodedJwt?.isEmailVerified, token, }; diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index af25f7208..6b9ab87d7 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -227,6 +227,7 @@ export const updateProjectQuery = ` name email walletAddress + isEmailVerified } } } diff --git a/test/testUtils.ts b/test/testUtils.ts index 0b4e9f7be..cd60366a0 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -92,6 +92,7 @@ export const generateTestAccessToken = async (id: number): Promise => { walletAddress: user?.walletAddress, name: user?.name, lastName: user?.lastName, + isEmailVerified: user?.isEmailVerified, }, config.get('JWT_SECRET') as string, { expiresIn: '30d' }, From 4b5a37d822eaeba77d4bae26ab989ed3f1d27b0b Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 09:25:12 +0100 Subject: [PATCH 08/12] fixing project resolver test --- src/resolvers/projectResolver.ts | 8 ++++++-- src/services/authorizationServices.ts | 3 --- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index ded592b28..5025787be 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1080,8 +1080,10 @@ export class ProjectResolver { i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); + const dbUser = await findUserById(user.userId); + // Check if user email is verified - if (!user.isEmailVerified) { + if (!dbUser || !dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } @@ -1368,8 +1370,10 @@ export class ProjectResolver { const user = await getLoggedInUser(ctx); const { image, description } = projectInput; + const dbUser = await findUserById(user.id); + // Check if user email is verified - if (!user.isEmailVerified) { + if (!dbUser || !dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index 43e995b3c..bc0221f1c 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -44,7 +44,6 @@ export interface JwtVerifiedUser { firstName?: string; lastName?: string; walletAddress?: string; - isEmailVerified?: boolean; userId: number; token: string; } @@ -82,7 +81,6 @@ export const validateImpactGraphJwt = async ( lastName: decodedJwt?.lastName, walletAddress: decodedJwt?.walletAddress, userId: decodedJwt?.userId, - isEmailVerified: decodedJwt?.isEmailVerified, token, }; @@ -121,7 +119,6 @@ export const validateAuthMicroserviceJwt = async ( name: user?.name, walletAddress: user?.walletAddress, userId: user!.id, - isEmailVerified: user?.isEmailVerified, token, }; } catch (e) { From 7708ba1e94d6819e537a052075c6a114c350e2e2 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 12:20:36 +0100 Subject: [PATCH 09/12] remove bypassing --- src/resolvers/userResolver.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 66ad07ae3..67d78bd59 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -136,7 +136,6 @@ export class UserResolver { @Arg('url', { nullable: true }) url: string, @Arg('avatar', { nullable: true }) avatar: string, @Arg('newUser', { nullable: true }) newUser: boolean, - @Arg('isFirstUpdate', { nullable: true }) isFirstUpdate: boolean, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user) @@ -175,11 +174,11 @@ export class UserResolver { dbUser.location = location; } // Check if user email is verified and it's not the first update - if (!dbUser.isEmailVerified && !isFirstUpdate) { + if (!dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } // Check if old email is verified and user entered new one and it's not the first update - if (dbUser.isEmailVerified && email !== dbUser.email && !isFirstUpdate) { + if (dbUser.isEmailVerified && email !== dbUser.email) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } if (email !== undefined) { From 105550a47151a9e4d39235ee394610aa56f99ec5 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 13:06:25 +0100 Subject: [PATCH 10/12] fixing tests --- test/graphqlQueries.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 6b9ab87d7..af25f7208 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -227,7 +227,6 @@ export const updateProjectQuery = ` name email walletAddress - isEmailVerified } } } From ae8716915a53b6d20c20a35fc858cd6035ca87b2 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 14:22:23 +0100 Subject: [PATCH 11/12] fixing project resolver test --- test/graphqlQueries.ts | 1 + test/testUtils.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index af25f7208..6b9ab87d7 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -227,6 +227,7 @@ export const updateProjectQuery = ` name email walletAddress + isEmailVerified } } } diff --git a/test/testUtils.ts b/test/testUtils.ts index cd60366a0..115305380 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -167,6 +167,7 @@ export const saveUserDirectlyToDb = async ( walletAddress, firstName: `testUser-${walletAddress}`, email: `testEmail-${walletAddress}@giveth.io`, + isEmailVerified: true, }).save(); }; From ddaef95de3a10d32674ec66d791fe2aa47db3caa Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 15:45:24 +0100 Subject: [PATCH 12/12] fixing user resolver test --- src/resolvers/userResolver.test.ts | 104 +++-------------------------- test/graphqlQueries.ts | 2 - 2 files changed, 8 insertions(+), 98 deletions(-) diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index fa76bea9d..58156e8ea 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -613,10 +613,10 @@ function updateUserTestCases() { const updateUserData = { firstName: 'firstName', lastName: 'lastName', - email: 'giveth@gievth.com', + location: 'location', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -652,10 +652,9 @@ function updateUserTestCases() { const updateUserData = { firstName: 'firstName', lastName: 'lastName', - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -690,7 +689,7 @@ function updateUserTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', }; @@ -712,66 +711,16 @@ function updateUserTestCases() { errorMessages.BOTH_FIRST_NAME_AND_LAST_NAME_CANT_BE_EMPTY, ); }); - it('should fail when email is invalid first case', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const accessToken = await generateTestAccessToken(user.id); - const updateUserData = { - firstName: 'firstName', - email: 'giveth', - avatar: 'pinata address', - url: 'website url', - isFirstUpdate: true, // bypassing verification of email - }; - const result = await axios.post( - graphqlUrl, - { - query: updateUser, - variables: updateUserData, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); - }); - it('should fail when email is invalid second case', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const accessToken = await generateTestAccessToken(user.id); - const updateUserData = { - firstName: 'firstName', - email: 'giveth @ giveth.com', - avatar: 'pinata address', - url: 'website url', - isFirstUpdate: true, // bypassing verification of email - }; - const result = await axios.post( - graphqlUrl, - { - query: updateUser, - variables: updateUserData, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); - }); it('should fail when sending empty string for firstName', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { firstName: '', lastName: 'test lastName', - email: 'giveth @ giveth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -797,10 +746,9 @@ function updateUserTestCases() { const updateUserData = { lastName: '', firstName: 'firstName', - email: 'giveth @ giveth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -829,11 +777,10 @@ function updateUserTestCases() { await user.save(); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', lastName: new Date().getTime().toString(), - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -869,11 +816,10 @@ function updateUserTestCases() { await user.save(); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', firstName: new Date().getTime().toString(), - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -900,40 +846,6 @@ function updateUserTestCases() { assert.equal(updatedUser?.name, updateUserData.firstName + ' ' + lastName); assert.equal(updatedUser?.lastName, lastName); }); - - it('should accept empty string for all fields except email', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const accessToken = await generateTestAccessToken(user.id); - const updateUserData = { - firstName: 'test firstName', - lastName: 'test lastName', - avatar: '', - url: '', - isFirstUpdate: true, // bypassing verification of email - }; - const result = await axios.post( - graphqlUrl, - { - query: updateUser, - variables: updateUserData, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.isTrue(result.data.data.updateUser); - const updatedUser = await User.findOne({ - where: { - id: user.id, - }, - }); - assert.equal(updatedUser?.firstName, updateUserData.firstName); - assert.equal(updatedUser?.lastName, updateUserData.lastName); - assert.equal(updatedUser?.avatar, updateUserData.avatar); - assert.equal(updatedUser?.url, updateUserData.url); - }); } function userEmailVerification() { diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 6b9ab87d7..a4e10f339 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1337,7 +1337,6 @@ export const updateUser = ` $firstName: String $avatar: String $newUser: Boolean - $isFirstUpdate: Boolean ) { updateUser( url: $url @@ -1347,7 +1346,6 @@ export const updateUser = ` lastName: $lastName avatar: $avatar newUser: $newUser - isFirstUpdate: $isFirstUpdate ) } `;