From 6e8e69a4ddd0dba79f3cd8bb686f4874796f3219 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Sun, 18 Aug 2024 02:57:08 +0330 Subject: [PATCH 01/12] Change email verification to use code instead of token --- .../notifications/MockNotificationAdapter.ts | 2 +- .../NotificationAdapterInterface.ts | 2 +- .../NotificationCenterAdapter.ts | 6 +- src/entities/user.ts | 4 +- src/repositories/userRepository.test.ts | 65 ++++++----------- src/repositories/userRepository.ts | 36 ++++------ src/resolvers/userResolver.test.ts | 45 ++++++------ src/resolvers/userResolver.ts | 69 +++++++------------ src/utils/errorMessages.ts | 4 ++ src/utils/locales/en.json | 3 +- src/utils/locales/es.json | 3 +- test/graphqlQueries.ts | 18 +++-- 12 files changed, 108 insertions(+), 149 deletions(-) diff --git a/src/adapters/notifications/MockNotificationAdapter.ts b/src/adapters/notifications/MockNotificationAdapter.ts index 97008f20a..4ded66c95 100644 --- a/src/adapters/notifications/MockNotificationAdapter.ts +++ b/src/adapters/notifications/MockNotificationAdapter.ts @@ -37,7 +37,7 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { async sendUserEmailConfirmation(params: { email: string; user: User; - token: string; + code: string; }) { logger.debug('MockNotificationAdapter sendUserEmailConfirmation', params); return Promise.resolve(undefined); diff --git a/src/adapters/notifications/NotificationAdapterInterface.ts b/src/adapters/notifications/NotificationAdapterInterface.ts index 827b168c4..29f501bac 100644 --- a/src/adapters/notifications/NotificationAdapterInterface.ts +++ b/src/adapters/notifications/NotificationAdapterInterface.ts @@ -65,7 +65,7 @@ export interface NotificationAdapterInterface { sendUserEmailConfirmation(params: { email: string; user: User; - token: string; + code: string; }): Promise; userSuperTokensCritical(params: { diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index e0200e8a4..834d4062a 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -95,16 +95,16 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { async sendUserEmailConfirmation(params: { email: string; user: User; - token: string; + code: string; }): Promise { - const { email, user, token } = params; + const { email, code } = params; try { await callSendNotification({ eventName: NOTIFICATIONS_EVENT_NAMES.SEND_EMAIL_CONFIRMATION, segment: { payload: { email, - verificationLink: `${dappUrl}/verification/user/${user.walletAddress}/${token}`, + verificationLink: code, // todo: we just set this for test and we should change the schema }, }, }); diff --git a/src/entities/user.ts b/src/entities/user.ts index 6fae47a1d..399244f71 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -194,11 +194,11 @@ export class User extends BaseEntity { @Field(_type => String, { nullable: true }) @Column('text', { nullable: true }) - emailConfirmationToken: string | null; + emailConfirmationCode: string | null; @Field(_type => Date, { nullable: true }) @Column('timestamptz', { nullable: true }) - emailConfirmationTokenExpiredAt: Date | null; + emailConfirmationCodeExpiredAt: Date | null; @Field(_type => Boolean, { nullable: true }) @Column({ default: false }) diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index 4b482d91b..d035d8250 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -11,14 +11,13 @@ import { User, UserRole } from '../entities/user'; import { findAdminUserByEmail, findAllUsers, - findUserByEmailConfirmationToken, findUserById, findUserByWalletAddress, findUsersWhoDonatedToProjectExcludeWhoLiked, findUsersWhoLikedProjectExcludeProjectOwner, findUsersWhoSupportProject, updateUserEmailConfirmationStatus, - updateUserEmailConfirmationToken, + updateUserEmailConfirmationCode, } from './userRepository'; import { Reaction } from '../entities/reaction'; @@ -47,17 +46,13 @@ describe( findUsersWhoDonatedToProjectTestCases, ); -describe( - 'userRepository.findUserByEmailConfirmationToken', - findUserByEmailConfirmationTokenTestCases, -); describe( 'userRepository.updateUserEmailConfirmationStatus', updateUserEmailConfirmationStatusTestCases, ); describe( - 'userRepository.updateUserEmailConfirmationToken', - updateUserEmailConfirmationTokenTestCases, + 'userRepository.updateUserEmailConfirmationCode', + updateUserEmailConfirmationCodeTestCases, ); function findUsersWhoDonatedToProjectTestCases() { @@ -506,40 +501,20 @@ function findUsersWhoSupportProjectTestCases() { }); } -function findUserByEmailConfirmationTokenTestCases() { - it('should return a user if a valid email confirmation token is provided', async () => { - await User.create({ - email: 'test@example.com', - emailConfirmationToken: 'validToken123', - loginType: 'wallet', - }).save(); - - const foundUser = await findUserByEmailConfirmationToken('validToken123'); - assert.isNotNull(foundUser); - assert.equal(foundUser!.email, 'test@example.com'); - assert.equal(foundUser!.emailConfirmationToken, 'validToken123'); - }); - - it('should return null if no user is found with the provided email confirmation token', async () => { - const foundUser = await findUserByEmailConfirmationToken('invalidToken123'); - assert.isNull(foundUser); - }); -} - function updateUserEmailConfirmationStatusTestCases() { it('should update the email confirmation status of a user', async () => { const user = await User.create({ email: 'test@example.com', emailConfirmed: false, - emailConfirmationToken: 'validToken123', + emailConfirmationCode: '234567', loginType: 'wallet', }).save(); await updateUserEmailConfirmationStatus({ userId: user.id, emailConfirmed: true, - emailConfirmationTokenExpiredAt: null, - emailConfirmationToken: null, + emailConfirmationCodeExpiredAt: null, + emailConfirmationCode: null, emailConfirmationSentAt: null, }); @@ -547,15 +522,15 @@ function updateUserEmailConfirmationStatusTestCases() { const updatedUser = await User.findOne({ where: { id: user.id } }); assert.isNotNull(updatedUser); assert.isTrue(updatedUser!.emailConfirmed); - assert.isNull(updatedUser!.emailConfirmationToken); + assert.isNull(updatedUser!.emailConfirmationCode); }); it('should not update any user if the userId does not exist', async () => { const result = await updateUserEmailConfirmationStatus({ userId: 999, // non-existent userId emailConfirmed: true, - emailConfirmationTokenExpiredAt: null, - emailConfirmationToken: null, + emailConfirmationCodeExpiredAt: null, + emailConfirmationCode: null, emailConfirmationSentAt: null, }); @@ -563,30 +538,30 @@ function updateUserEmailConfirmationStatusTestCases() { }); } -function updateUserEmailConfirmationTokenTestCases() { - it('should update the email confirmation token and expiry date for a user', async () => { +function updateUserEmailConfirmationCodeTestCases() { + it('should update the email confirmation code and expiry date for a user', async () => { const user = await User.create({ email: 'test@example.com', loginType: 'wallet', }).save(); - const newToken = 'newToken123'; + const newCode = '654321'; const newExpiryDate = new Date(Date.now() + 3600 * 1000); // 1 hour from now const sentAtDate = new Date(); - await updateUserEmailConfirmationToken({ + await updateUserEmailConfirmationCode({ userId: user.id, - emailConfirmationToken: newToken, - emailConfirmationTokenExpiredAt: newExpiryDate, + emailConfirmationCode: newCode, + emailConfirmationCodeExpiredAt: newExpiryDate, emailConfirmationSentAt: sentAtDate, }); // Using findOne with options object const updatedUser = await User.findOne({ where: { id: user.id } }); assert.isNotNull(updatedUser); - assert.equal(updatedUser!.emailConfirmationToken, newToken); + assert.equal(updatedUser!.emailConfirmationCode, newCode); assert.equal( - updatedUser!.emailConfirmationTokenExpiredAt!.getTime(), + updatedUser!.emailConfirmationCodeExpiredAt!.getTime(), newExpiryDate.getTime(), ); assert.equal( @@ -597,10 +572,10 @@ function updateUserEmailConfirmationTokenTestCases() { it('should throw an error if the userId does not exist', async () => { try { - await updateUserEmailConfirmationToken({ + await updateUserEmailConfirmationCode({ userId: 999, // non-existent userId - emailConfirmationToken: 'newToken123', - emailConfirmationTokenExpiredAt: new Date(), + emailConfirmationCode: '765432', + emailConfirmationCodeExpiredAt: new Date(), emailConfirmationSentAt: new Date(), }); assert.fail('Expected an error to be thrown'); diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 985926596..65ac73596 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -179,28 +179,18 @@ export const findUsersWhoSupportProject = async ( return users; }; -export const findUserByEmailConfirmationToken = async ( - emailConfirmationToken: string, -): Promise => { - return User.createQueryBuilder('user') - .where({ - emailConfirmationToken, - }) - .getOne(); -}; - export const updateUserEmailConfirmationStatus = async (params: { userId: number; emailConfirmed: boolean; - emailConfirmationTokenExpiredAt: Date | null; - emailConfirmationToken: string | null; + emailConfirmationCodeExpiredAt: Date | null; + emailConfirmationCode: string | null; emailConfirmationSentAt: Date | null; }): Promise => { const { userId, emailConfirmed, - emailConfirmationTokenExpiredAt, - emailConfirmationToken, + emailConfirmationCodeExpiredAt, + emailConfirmationCode, emailConfirmationSentAt, } = params; @@ -208,24 +198,24 @@ export const updateUserEmailConfirmationStatus = async (params: { .update(User) .set({ emailConfirmed, - emailConfirmationTokenExpiredAt, - emailConfirmationToken, + emailConfirmationCodeExpiredAt, + emailConfirmationCode, emailConfirmationSentAt, }) .where('id = :userId', { userId }) .execute(); }; -export const updateUserEmailConfirmationToken = async (params: { +export const updateUserEmailConfirmationCode = async (params: { userId: number; - emailConfirmationToken: string; - emailConfirmationTokenExpiredAt: Date; + emailConfirmationCode: string; + emailConfirmationCodeExpiredAt: Date; emailConfirmationSentAt: Date; }): Promise => { const { userId, - emailConfirmationToken, - emailConfirmationTokenExpiredAt, + emailConfirmationCode, + emailConfirmationCodeExpiredAt, emailConfirmationSentAt, } = params; @@ -234,8 +224,8 @@ export const updateUserEmailConfirmationToken = async (params: { throw new Error('User not found'); } - user.emailConfirmationToken = emailConfirmationToken; - user.emailConfirmationTokenExpiredAt = emailConfirmationTokenExpiredAt; + user.emailConfirmationCode = emailConfirmationCode; + user.emailConfirmationCodeExpiredAt = emailConfirmationCodeExpiredAt; user.emailConfirmationSentAt = emailConfirmationSentAt; user.emailConfirmed = false; diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index 38772227a..7673a8351 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -6,7 +6,6 @@ import { User } from '../entities/user'; import { createDonationData, createProjectData, - generateConfirmationEmailToken, generateRandomEtheriumAddress, generateTestAccessToken, graphqlUrl, @@ -26,8 +25,6 @@ import { errorMessages } from '../utils/errorMessages'; import { DONATION_STATUS } from '../entities/donation'; import { getGitcoinAdapter } from '../adapters/adaptersFactory'; import { updateUserTotalDonated } from '../services/userService'; -import { findUserById } from '../repositories/userRepository'; -import { sleep } from '../utils/utils'; describe('updateUser() test cases', updateUserTestCases); describe('userByAddress() test cases', userByAddressTestCases); @@ -710,7 +707,7 @@ function userVerificationSendEmailConfirmationTestCases() { ); assert.isNotNull( result.data.data.userVerificationSendEmailConfirmation - .emailConfirmationToken, + .emailConfirmationCode, ); }); @@ -766,16 +763,17 @@ function userVerificationConfirmEmailTestCases() { }, ); - const token = + const code = emailConfirmationSentResult.data.data - .userVerificationSendEmailConfirmation.emailConfirmationToken; + .userVerificationSendEmailConfirmation.emailConfirmationCode; const result = await axios.post( graphqlUrl, { query: userVerificationConfirmEmail, variables: { - emailConfirmationToken: token, + userId: user.id, + emailConfirmationCode: code, }, }, { @@ -795,24 +793,37 @@ function userVerificationConfirmEmailTestCases() { ); }); - it('should throw error when confirm email token is invalid or expired for user email verification', async () => { + it('should throw error when email confirmation code is incorrect for user email verification', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); user.email = 'test@example.com'; user.emailConfirmed = false; await user.save(); const accessToken = await generateTestAccessToken(user.id); - const token = await generateConfirmationEmailToken(user.id); - user.emailConfirmationToken = token; - await user.save(); - await sleep(500); // Simulating token expiration or invalidity + await axios.post( + graphqlUrl, + { + query: userVerificationSendEmailConfirmation, + variables: { + userId: user.id, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + const incorrectCode = '12345'; // This code is incorrect const result = await axios.post( graphqlUrl, { query: userVerificationConfirmEmail, variables: { - emailConfirmationToken: token, + userId: user.id, + emailConfirmationCode: incorrectCode, }, }, { @@ -822,12 +833,6 @@ function userVerificationConfirmEmailTestCases() { }, ); - assert.equal(result.data.errors[0].message, 'jwt expired'); - const userReinitializedEmailParams = await findUserById(user.id); - - assert.isFalse(userReinitializedEmailParams!.emailConfirmed); - assert.isFalse(userReinitializedEmailParams!.emailConfirmationSent); - assert.isNotOk(userReinitializedEmailParams!.emailConfirmationSentAt); - assert.isNull(userReinitializedEmailParams!.emailConfirmationToken); + assert.equal(result.data.errors[0].message, errorMessages.INCORRECT_CODE); }); } diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index a6150ab55..8ebc3164e 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -8,17 +8,14 @@ import { Resolver, } from 'type-graphql'; import { Repository } from 'typeorm'; -import * as jwt from 'jsonwebtoken'; import moment from 'moment'; import { User } from '../entities/user'; -import config from '../config'; import { AccountVerificationInput } from './types/accountVerificationInput'; import { ApolloContext } from '../types/ApolloContext'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { validateEmail } from '../utils/validators/commonValidators'; import { - findUserByEmailConfirmationToken, findUserById, findUserByWalletAddress, } from '../repositories/userRepository'; @@ -183,8 +180,8 @@ export class UserResolver { if (dbUser.email !== email) { dbUser.emailConfirmed = false; dbUser.emailConfirmationSent = false; - dbUser.emailConfirmationToken = null; - dbUser.emailConfirmationTokenExpiredAt = null; + dbUser.emailConfirmationCode = null; + dbUser.emailConfirmationCodeExpiredAt = null; dbUser.emailConfirmationSentAt = null; dbUser.emailConfirmedAt = null; } @@ -273,16 +270,12 @@ export class UserResolver { ); } - const token = jwt.sign( - { userId }, - config.get('MAILER_JWT_SECRET') as string, - { expiresIn: '5m' }, - ); + const code = Math.floor(100000 + Math.random() * 900000).toString(); - userToVerify.emailConfirmationTokenExpiredAt = moment() + userToVerify.emailConfirmationCodeExpiredAt = moment() .add(5, 'minutes') .toDate(); - userToVerify.emailConfirmationToken = token; + userToVerify.emailConfirmationCode = code; userToVerify.emailConfirmationSent = true; userToVerify.emailConfirmed = false; userToVerify.emailConfirmationSentAt = new Date(); @@ -291,7 +284,7 @@ export class UserResolver { await getNotificationAdapter().sendUserEmailConfirmation({ email, user: userToVerify, - token, + code, }); return userToVerify; @@ -303,50 +296,34 @@ export class UserResolver { @Mutation(_returns => User) async userVerificationConfirmEmail( - @Arg('emailConfirmationToken') emailConfirmationToken: string, + @Arg('userId') userId: number, + @Arg('emailConfirmationCode') emailConfirmationCode: string, + @Ctx() { req: { user } }: ApolloContext, ): Promise { try { - const secret = config.get('MAILER_JWT_SECRET') as string; - - const isValidToken = await findUserByEmailConfirmationToken( - emailConfirmationToken, - ); - - if (!isValidToken) { - throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + const currentUserId = user?.userId; + if (!currentUserId || currentUserId != userId) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); } - const decodedJwt: any = jwt.verify(emailConfirmationToken, secret); - const userId = decodedJwt.userId; - const user = await findUserById(userId); + const userFromDB = await findUserById(userId); - if (!user) { + if (!userFromDB) { throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); } - user.emailConfirmationTokenExpiredAt = null; - user.emailConfirmationToken = null; - user.emailConfirmedAt = new Date(); - user.emailConfirmed = true; - await user.save(); - - return user; - } catch (e) { - const user = await findUserByEmailConfirmationToken( - emailConfirmationToken, - ); - - if (!user) { - throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + if (emailConfirmationCode !== userFromDB.emailConfirmationCode) { + throw new Error(i18n.__(translationErrorMessagesKeys.INCORRECT_CODE)); } - user.emailConfirmed = false; - user.emailConfirmationTokenExpiredAt = null; - user.emailConfirmationSent = false; - user.emailConfirmationSentAt = null; - user.emailConfirmationToken = null; + userFromDB.emailConfirmationCodeExpiredAt = null; + userFromDB.emailConfirmationCode = null; + userFromDB.emailConfirmedAt = new Date(); + userFromDB.emailConfirmed = true; + await userFromDB.save(); - await user.save(); + return userFromDB; + } catch (e) { logger.error('userVerificationConfirmEmail() error', e); throw e; } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 2447e821b..1197a4b93 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -175,6 +175,9 @@ export const errorMessages = { TX_NOT_FOUND: 'Transaction not found', INVALID_PROJECT_ID: 'Invalid project id', INVALID_PROJECT_OWNER: 'Project owner is invalid', + + NO_EMAIL_PROVIDED: 'No email address provided.', + INCORRECT_CODE: 'The verification code you entered is incorrect.', }; export const translationErrorMessagesKeys = { @@ -320,4 +323,5 @@ export const translationErrorMessagesKeys = { EVM_SUPPORT_ONLY: 'EVM_SUPPORT_ONLY', NO_EMAIL_PROVIDED: 'NO_EMAIL_PROVIDED', + INCORRECT_CODE: 'INCORRECT_CODE', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index eab637fc7..3b947d3d6 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -102,5 +102,6 @@ "INVALID_PROJECT_ID": "INVALID_PROJECT_ID", "TX_NOT_FOUND": "TX_NOT_FOUND", - "NO_EMAIL_PROVIDED": "No email address provided." + "NO_EMAIL_PROVIDED": "No email address provided.", + "INCORRECT_CODE": "The verification code you entered is incorrect." } \ No newline at end of file diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index c83c53956..d00214f3d 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -99,5 +99,6 @@ "DRAFT_DONATION_DISABLED": "El borrador de donación está deshabilitado", "EVM_SUPPORT_ONLY": "Solo se admite EVM", - "NO_EMAIL_PROVIDED": "No se ha proporcionado una dirección de correo electrónico." + "NO_EMAIL_PROVIDED": "No se ha proporcionado una dirección de correo electrónico.", + "INCORRECT_CODE": "El código de verificación que ingresaste es incorrecto." } diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 34a5bfcf1..cbaf802c3 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1981,8 +1981,8 @@ export const userVerificationSendEmailConfirmation = ` id email emailConfirmed - emailConfirmationToken - emailConfirmationTokenExpiredAt + emailConfirmationCode + emailConfirmationCodeExpiredAt emailConfirmationSent emailConfirmationSentAt emailConfirmedAt @@ -1991,13 +1991,19 @@ export const userVerificationSendEmailConfirmation = ` `; export const userVerificationConfirmEmail = ` - mutation userVerificationConfirmEmail($emailConfirmationToken: String!){ - userVerificationConfirmEmail(emailConfirmationToken: $emailConfirmationToken) { + mutation userVerificationConfirmEmail( + $userId: Float! + $emailConfirmationCode: String! + ){ + userVerificationConfirmEmail( + userId: $userId + emailConfirmationCode: $emailConfirmationCode + ) { id email emailConfirmed - emailConfirmationToken - emailConfirmationTokenExpiredAt + emailConfirmationCode + emailConfirmationCodeExpiredAt emailConfirmationSent emailConfirmationSentAt emailConfirmedAt From 81af731934a8d4a0475cf93ebb3593c11d143754 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Sun, 18 Aug 2024 02:57:51 +0330 Subject: [PATCH 02/12] Add migration for rename fields in user table --- ...96571-RenameUserEmailVerificationFields.ts | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 migration/1723936796571-RenameUserEmailVerificationFields.ts diff --git a/migration/1723936796571-RenameUserEmailVerificationFields.ts b/migration/1723936796571-RenameUserEmailVerificationFields.ts new file mode 100644 index 000000000..3871f5c1d --- /dev/null +++ b/migration/1723936796571-RenameUserEmailVerificationFields.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameUserEmailVerificationFields1723936796571 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // Rename emailConfirmationToken to emailVerificationCode + await queryRunner.renameColumn( + 'user', + 'emailConfirmationToken', + 'emailVerificationCode', + ); + + // Rename emailConfirmationTokenExpiredAt to emailVerificationCodeExpiredAt + await queryRunner.renameColumn( + 'user', + 'emailConfirmationTokenExpiredAt', + 'emailVerificationCodeExpiredAt', + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Revert emailVerificationCode back to emailConfirmationToken + await queryRunner.renameColumn( + 'user', + 'emailVerificationCode', + 'emailConfirmationToken', + ); + + // Revert emailVerificationCodeExpiredAt back to emailConfirmationTokenExpiredAt + await queryRunner.renameColumn( + 'user', + 'emailVerificationCodeExpiredAt', + 'emailConfirmationTokenExpiredAt', + ); + } +} From e9af716b85162ffba362cd8d3aec7c91bb92ec31 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Sun, 18 Aug 2024 16:56:34 +0330 Subject: [PATCH 03/12] remove code from result --- src/resolvers/userResolver.test.ts | 13 ++++++++----- test/graphqlQueries.ts | 4 ---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index 7673a8351..e0d446467 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -745,10 +745,10 @@ function userVerificationConfirmEmailTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); user.email = 'test@example.com'; user.emailConfirmed = false; - await user.save(); + const userID = (await user.save()).id; const accessToken = await generateTestAccessToken(user.id); - const emailConfirmationSentResult = await axios.post( + await axios.post( graphqlUrl, { query: userVerificationSendEmailConfirmation, @@ -763,9 +763,12 @@ function userVerificationConfirmEmailTestCases() { }, ); - const code = - emailConfirmationSentResult.data.data - .userVerificationSendEmailConfirmation.emailConfirmationCode; + const DBUser = await User.findOne({ + where: { + id: userID, + }, + }); + const code = DBUser?.emailConfirmationCode; const result = await axios.post( graphqlUrl, diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 89826c1cc..9cd78c158 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1996,8 +1996,6 @@ export const userVerificationSendEmailConfirmation = ` id email emailConfirmed - emailConfirmationCode - emailConfirmationCodeExpiredAt emailConfirmationSent emailConfirmationSentAt emailConfirmedAt @@ -2017,8 +2015,6 @@ export const userVerificationConfirmEmail = ` id email emailConfirmed - emailConfirmationCode - emailConfirmationCodeExpiredAt emailConfirmationSent emailConfirmationSentAt emailConfirmedAt From 8353826d837d7ab63fc0e40fcf9b872f8fe1afe4 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 04:11:03 +0330 Subject: [PATCH 04/12] delete migrations --- ...83534955-AddUserEmailVerificationFields.ts | 29 --------------- ...96571-RenameUserEmailVerificationFields.ts | 37 ------------------- 2 files changed, 66 deletions(-) delete mode 100644 migration/1723583534955-AddUserEmailVerificationFields.ts delete mode 100644 migration/1723936796571-RenameUserEmailVerificationFields.ts diff --git a/migration/1723583534955-AddUserEmailVerificationFields.ts b/migration/1723583534955-AddUserEmailVerificationFields.ts deleted file mode 100644 index 07921e89e..000000000 --- a/migration/1723583534955-AddUserEmailVerificationFields.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class AddUserEmailVerificationFields1723583534955 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "user" - ADD "emailConfirmationToken" character varying, - ADD "emailConfirmationTokenExpiredAt" TIMESTAMP, - ADD "emailConfirmed" boolean DEFAULT false, - ADD "emailConfirmationSent" boolean DEFAULT false, - ADD "emailConfirmationSentAt" TIMESTAMP, - ADD "emailConfirmedAt" TIMESTAMP; - `); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "user" - DROP COLUMN "emailConfirmationToken", - DROP COLUMN "emailConfirmationTokenExpiredAt", - DROP COLUMN "emailConfirmed", - DROP COLUMN "emailConfirmationSent", - DROP COLUMN "emailConfirmationSentAt", - DROP COLUMN "emailConfirmedAt"; - `); - } -} diff --git a/migration/1723936796571-RenameUserEmailVerificationFields.ts b/migration/1723936796571-RenameUserEmailVerificationFields.ts deleted file mode 100644 index 3871f5c1d..000000000 --- a/migration/1723936796571-RenameUserEmailVerificationFields.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; - -export class RenameUserEmailVerificationFields1723936796571 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - // Rename emailConfirmationToken to emailVerificationCode - await queryRunner.renameColumn( - 'user', - 'emailConfirmationToken', - 'emailVerificationCode', - ); - - // Rename emailConfirmationTokenExpiredAt to emailVerificationCodeExpiredAt - await queryRunner.renameColumn( - 'user', - 'emailConfirmationTokenExpiredAt', - 'emailVerificationCodeExpiredAt', - ); - } - - public async down(queryRunner: QueryRunner): Promise { - // Revert emailVerificationCode back to emailConfirmationToken - await queryRunner.renameColumn( - 'user', - 'emailVerificationCode', - 'emailConfirmationToken', - ); - - // Revert emailVerificationCodeExpiredAt back to emailConfirmationTokenExpiredAt - await queryRunner.renameColumn( - 'user', - 'emailVerificationCodeExpiredAt', - 'emailConfirmationTokenExpiredAt', - ); - } -} From ee39b2fefa4d9c98e52990bccc8f284b359f13c7 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 05:40:23 +0330 Subject: [PATCH 05/12] Separate email verification details from user data in a new table --- src/entities/user.ts | 17 +- src/entities/userEmailVerification.ts | 23 +++ src/repositories/userRepository.test.ts | 205 +++++++++++++++++++----- src/repositories/userRepository.ts | 65 ++++---- src/resolvers/userResolver.test.ts | 78 +++++++-- src/resolvers/userResolver.ts | 72 ++++++--- src/utils/errorMessages.ts | 4 + src/utils/locales/en.json | 4 +- src/utils/locales/es.json | 6 +- 9 files changed, 359 insertions(+), 115 deletions(-) create mode 100644 src/entities/userEmailVerification.ts diff --git a/src/entities/user.ts b/src/entities/user.ts index 399244f71..061751721 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -18,6 +18,7 @@ import { ProjectStatusHistory } from './projectStatusHistory'; import { ProjectVerificationForm } from './projectVerificationForm'; import { ReferredEvent } from './referredEvent'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; +import { UserEmailVerification } from './userEmailVerification'; export const publicSelectionFields = [ 'user.id', @@ -192,17 +193,9 @@ export class User extends BaseEntity { @Column({ default: false }) emailConfirmed: boolean; - @Field(_type => String, { nullable: true }) - @Column('text', { nullable: true }) - emailConfirmationCode: string | null; - - @Field(_type => Date, { nullable: true }) - @Column('timestamptz', { nullable: true }) - emailConfirmationCodeExpiredAt: Date | null; - @Field(_type => Boolean, { nullable: true }) @Column({ default: false }) - emailConfirmationSent: boolean; + emailConfirmationSent: boolean | null; @Field(_type => Date, { nullable: true }) @Column({ type: 'timestamptz', nullable: true }) @@ -212,6 +205,12 @@ export class User extends BaseEntity { @Column({ type: 'timestamptz', nullable: true }) emailConfirmedAt: Date | null; + @OneToOne( + () => UserEmailVerification, + emailVerification => emailVerification.user, + ) + emailVerification: UserEmailVerification; + @Field(_type => Int, { nullable: true }) async donationsCount() { return await Donation.createQueryBuilder('donation') diff --git a/src/entities/userEmailVerification.ts b/src/entities/userEmailVerification.ts new file mode 100644 index 000000000..9c94d7bd0 --- /dev/null +++ b/src/entities/userEmailVerification.ts @@ -0,0 +1,23 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + OneToOne, + BaseEntity, +} from 'typeorm'; +import { User } from './user'; + +@Entity() +export class UserEmailVerification extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @OneToOne(() => User, user => user.emailVerification, { onDelete: 'CASCADE' }) + user: User; + + @Column({ nullable: true }) + emailVerificationCode: string | null; + + @Column('timestamptz', { nullable: true }) + emailVerificationCodeExpiredAt: Date | null; +} diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index d035d8250..63d07e245 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -16,10 +16,11 @@ import { findUsersWhoDonatedToProjectExcludeWhoLiked, findUsersWhoLikedProjectExcludeProjectOwner, findUsersWhoSupportProject, + getUserEmailConfirmationFields, updateUserEmailConfirmationStatus, - updateUserEmailConfirmationCode, } from './userRepository'; import { Reaction } from '../entities/reaction'; +import { UserEmailVerification } from '../entities/userEmailVerification'; describe('sql injection test cases', sqlInjectionTestCases); @@ -47,12 +48,12 @@ describe( ); describe( - 'userRepository.updateUserEmailConfirmationStatus', + 'updateUserEmailConfirmationStatus() test cases', updateUserEmailConfirmationStatusTestCases, ); describe( - 'userRepository.updateUserEmailConfirmationCode', - updateUserEmailConfirmationCodeTestCases, + 'getUserEmailConfirmationFields() test cases', + getUserEmailConfirmationFieldsTestCases, ); function findUsersWhoDonatedToProjectTestCases() { @@ -506,81 +507,197 @@ function updateUserEmailConfirmationStatusTestCases() { const user = await User.create({ email: 'test@example.com', emailConfirmed: false, - emailConfirmationCode: '234567', loginType: 'wallet', }).save(); + await UserEmailVerification.create({ + user: user, + emailVerificationCode: '234567', + emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), + }).save(); + await updateUserEmailConfirmationStatus({ userId: user.id, emailConfirmed: true, - emailConfirmationCodeExpiredAt: null, - emailConfirmationCode: null, - emailConfirmationSentAt: null, + emailConfirmedAt: new Date(), + emailVerificationCodeExpiredAt: null, + emailVerificationCode: null, + emailConfirmationSent: false, // Include this property + emailConfirmationSentAt: null, // Include this property + }); + + // Verify changes in UserEmailVerification table + const updatedVerification = await UserEmailVerification.findOne({ + where: { user: { id: user.id } }, }); - // Using findOne with options object + assert.isNotNull(updatedVerification); + assert.isNull(updatedVerification!.emailVerificationCode); + assert.isNull(updatedVerification!.emailVerificationCodeExpiredAt); + + // Verify changes in User table const updatedUser = await User.findOne({ where: { id: user.id } }); assert.isNotNull(updatedUser); assert.isTrue(updatedUser!.emailConfirmed); - assert.isNull(updatedUser!.emailConfirmationCode); + assert.isNotNull(updatedUser!.emailConfirmedAt); + assert.isFalse(updatedUser!.emailConfirmationSent); + assert.isNull(updatedUser!.emailConfirmationSentAt); + }); + + it('should create a new UserEmailVerification entry if it does not exist and update the email confirmation status', async () => { + const user = await User.create({ + email: 'test2@example.com', + emailConfirmed: false, + loginType: 'wallet', + }).save(); + + await updateUserEmailConfirmationStatus({ + userId: user.id, + emailConfirmed: true, + emailConfirmedAt: new Date(), + emailVerificationCodeExpiredAt: null, + emailVerificationCode: null, + emailConfirmationSent: false, // Include this property + emailConfirmationSentAt: null, // Include this property + }); + + // Verify new entry in UserEmailVerification table + const newVerification = await UserEmailVerification.findOne({ + where: { user: { id: user.id } }, + }); + + assert.isNotNull(newVerification); + assert.isNull(newVerification!.emailVerificationCode); + assert.isNull(newVerification!.emailVerificationCodeExpiredAt); + + // Verify changes in User table + const updatedUser = await User.findOne({ where: { id: user.id } }); + assert.isNotNull(updatedUser); + assert.isTrue(updatedUser!.emailConfirmed); + assert.isNotNull(updatedUser!.emailConfirmedAt); + assert.isFalse(updatedUser!.emailConfirmationSent); + assert.isNull(updatedUser!.emailConfirmationSentAt); }); it('should not update any user if the userId does not exist', async () => { const result = await updateUserEmailConfirmationStatus({ userId: 999, // non-existent userId emailConfirmed: true, - emailConfirmationCodeExpiredAt: null, - emailConfirmationCode: null, - emailConfirmationSentAt: null, + emailConfirmedAt: new Date(), + emailVerificationCodeExpiredAt: null, + emailVerificationCode: null, + emailConfirmationSent: false, // Include this property + emailConfirmationSentAt: null, // Include this property }); assert.equal(result.affected, 0); // No rows should be affected }); } -function updateUserEmailConfirmationCodeTestCases() { - it('should update the email confirmation code and expiry date for a user', async () => { +function getUserEmailConfirmationFieldsTestCases() { + it('should return the email verification fields for a valid user ID', async () => { const user = await User.create({ email: 'test@example.com', loginType: 'wallet', + emailConfirmationSent: true, + emailConfirmationSentAt: new Date(), }).save(); - const newCode = '654321'; - const newExpiryDate = new Date(Date.now() + 3600 * 1000); // 1 hour from now - const sentAtDate = new Date(); + const emailVerification = await UserEmailVerification.create({ + user: user, + emailVerificationCode: '123456', + emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), // 1 hour from now + }).save(); - await updateUserEmailConfirmationCode({ - userId: user.id, - emailConfirmationCode: newCode, - emailConfirmationCodeExpiredAt: newExpiryDate, - emailConfirmationSentAt: sentAtDate, - }); + const result = await getUserEmailConfirmationFields(user.id); - // Using findOne with options object - const updatedUser = await User.findOne({ where: { id: user.id } }); - assert.isNotNull(updatedUser); - assert.equal(updatedUser!.emailConfirmationCode, newCode); + assert.isNotNull(result); + assert.equal(result?.emailVerificationCode, '123456'); assert.equal( - updatedUser!.emailConfirmationCodeExpiredAt!.getTime(), - newExpiryDate.getTime(), + result?.emailVerificationCodeExpiredAt!.getTime(), + emailVerification.emailVerificationCodeExpiredAt!.getTime(), ); + assert.equal(user.emailConfirmationSent, true); assert.equal( - updatedUser!.emailConfirmationSentAt!.getTime(), - sentAtDate.getTime(), + user.emailConfirmationSentAt!.getTime(), + user.emailConfirmationSentAt!.getTime(), ); }); - it('should throw an error if the userId does not exist', async () => { - try { - await updateUserEmailConfirmationCode({ - userId: 999, // non-existent userId - emailConfirmationCode: '765432', - emailConfirmationCodeExpiredAt: new Date(), - emailConfirmationSentAt: new Date(), - }); - assert.fail('Expected an error to be thrown'); - } catch (error) { - assert.equal(error.message, 'User not found'); - } + it('should return null if no email verification entry exists for the user ID', async () => { + const user = await User.create({ + email: 'test2@example.com', + loginType: 'wallet', + emailConfirmationSent: false, + emailConfirmationSentAt: null, + }).save(); + + const result = await getUserEmailConfirmationFields(user.id); + + assert.isNull(result); + }); + + it('should return null if the user ID does not exist', async () => { + const result = await getUserEmailConfirmationFields(999); // non-existent user ID + + assert.isNull(result); + }); + + it('should return the correct fields for a user with a different emailVerificationCode', async () => { + const user = await User.create({ + email: 'test3@example.com', + loginType: 'wallet', + emailConfirmationSent: true, + emailConfirmationSentAt: new Date(Date.now() - 3600 * 1000), // 1 hour ago + }).save(); + + const emailVerification = await UserEmailVerification.create({ + user: user, + emailVerificationCode: '654321', + emailVerificationCodeExpiredAt: new Date(Date.now() + 7200 * 1000), // 2 hours from now + }).save(); + + const result = await getUserEmailConfirmationFields(user.id); + + assert.isNotNull(result); + assert.equal(result?.emailVerificationCode, '654321'); + assert.equal( + result?.emailVerificationCodeExpiredAt!.getTime(), + emailVerification.emailVerificationCodeExpiredAt!.getTime(), + ); + assert.equal(user.emailConfirmationSent, true); + assert.equal( + user.emailConfirmationSentAt!.getTime(), + user.emailConfirmationSentAt!.getTime(), + ); + }); + + it('should return the correct fields when emailConfirmationSent is false', async () => { + const user = await User.create({ + email: 'test4@example.com', + loginType: 'wallet', + emailConfirmationSent: false, + emailConfirmationSentAt: new Date(), + }).save(); + + const emailVerification = await UserEmailVerification.create({ + user: user, + emailVerificationCode: '111111', + emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), // 1 hour from now + }).save(); + + const result = await getUserEmailConfirmationFields(user.id); + + assert.isNotNull(result); + assert.equal(result?.emailVerificationCode, '111111'); + assert.equal( + result?.emailVerificationCodeExpiredAt!.getTime(), + emailVerification.emailVerificationCodeExpiredAt!.getTime(), + ); + assert.equal(user.emailConfirmationSent, false); + assert.equal( + user.emailConfirmationSentAt!.getTime(), + user.emailConfirmationSentAt!.getTime(), + ); }); } diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 65ac73596..910eef312 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -5,6 +5,7 @@ import { Reaction } from '../entities/reaction'; import { Project, ProjStatus, ReviewStatus } from '../entities/project'; import { isEvmAddress } from '../utils/networks'; import { retrieveActiveQfRoundUserMBDScore } from './qfRoundRepository'; +import { UserEmailVerification } from '../entities/userEmailVerification'; export const findAdminUserByEmail = async ( email: string, @@ -182,53 +183,57 @@ export const findUsersWhoSupportProject = async ( export const updateUserEmailConfirmationStatus = async (params: { userId: number; emailConfirmed: boolean; - emailConfirmationCodeExpiredAt: Date | null; - emailConfirmationCode: string | null; + emailConfirmedAt: Date | null; + emailVerificationCodeExpiredAt: Date | null; + emailVerificationCode: string | null; + emailConfirmationSent: boolean | null; emailConfirmationSentAt: Date | null; }): Promise => { const { userId, emailConfirmed, - emailConfirmationCodeExpiredAt, - emailConfirmationCode, + emailConfirmedAt, + emailVerificationCodeExpiredAt, + emailVerificationCode, + emailConfirmationSent, emailConfirmationSentAt, } = params; + let userVerification = await UserEmailVerification.findOne({ + where: { user: { id: userId } }, + }); + + if (!userVerification) { + userVerification = new UserEmailVerification(); + userVerification.user = { id: userId } as User; // Creating a new association with the user + } + + userVerification.emailVerificationCode = emailVerificationCode; + userVerification.emailVerificationCodeExpiredAt = + emailVerificationCodeExpiredAt; + + await UserEmailVerification.save(userVerification); + + // Update the emailConfirmed status in the User table return User.createQueryBuilder() .update(User) .set({ emailConfirmed, - emailConfirmationCodeExpiredAt, - emailConfirmationCode, + emailConfirmedAt, + emailConfirmationSent, emailConfirmationSentAt, }) .where('id = :userId', { userId }) .execute(); }; -export const updateUserEmailConfirmationCode = async (params: { - userId: number; - emailConfirmationCode: string; - emailConfirmationCodeExpiredAt: Date; - emailConfirmationSentAt: Date; -}): Promise => { - const { - userId, - emailConfirmationCode, - emailConfirmationCodeExpiredAt, - emailConfirmationSentAt, - } = params; - - const user = await findUserById(userId); - if (!user) { - throw new Error('User not found'); - } - - user.emailConfirmationCode = emailConfirmationCode; - user.emailConfirmationCodeExpiredAt = emailConfirmationCodeExpiredAt; - user.emailConfirmationSentAt = emailConfirmationSentAt; - user.emailConfirmed = false; +export const getUserEmailConfirmationFields = async ( + userId: number, +): Promise => { + // Find the email verification entry for the given user ID + const emailVerification = await UserEmailVerification.findOne({ + where: { user: { id: userId } }, + }); - await user.save(); - return user; + return emailVerification || null; }; diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index e0d446467..d6bdf05ce 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -25,6 +25,8 @@ import { errorMessages } from '../utils/errorMessages'; import { DONATION_STATUS } from '../entities/donation'; import { getGitcoinAdapter } from '../adapters/adaptersFactory'; import { updateUserTotalDonated } from '../services/userService'; +import { getUserEmailConfirmationFields } from '../repositories/userRepository'; +import { UserEmailVerification } from '../entities/userEmailVerification'; describe('updateUser() test cases', updateUserTestCases); describe('userByAddress() test cases', userByAddressTestCases); @@ -705,10 +707,13 @@ function userVerificationSendEmailConfirmationTestCases() { .emailConfirmationSent, true, ); - assert.isNotNull( - result.data.data.userVerificationSendEmailConfirmation - .emailConfirmationCode, + + const emailConfirmationFields = await getUserEmailConfirmationFields( + user.id, ); + assert.isNotNull(emailConfirmationFields); + assert.equal(emailConfirmationFields?.emailVerificationCode?.length, 6); + assert.isNotNull(emailConfirmationFields?.emailVerificationCodeExpiredAt); }); it('should throw error when sending email confirmation if email is already confirmed', async () => { @@ -763,12 +768,9 @@ function userVerificationConfirmEmailTestCases() { }, ); - const DBUser = await User.findOne({ - where: { - id: userID, - }, - }); - const code = DBUser?.emailConfirmationCode; + const emailVerificationFields = + await getUserEmailConfirmationFields(userID); + const code = emailVerificationFields?.emailVerificationCode; const result = await axios.post( graphqlUrl, @@ -794,6 +796,11 @@ function userVerificationConfirmEmailTestCases() { assert.isNotNull( result.data.data.userVerificationConfirmEmail.emailConfirmedAt, ); + + const updatedVerificationFields = + await getUserEmailConfirmationFields(userID); + assert.isNull(updatedVerificationFields?.emailVerificationCode); + assert.isNull(updatedVerificationFields?.emailVerificationCodeExpiredAt); }); it('should throw error when email confirmation code is incorrect for user email verification', async () => { @@ -818,7 +825,7 @@ function userVerificationConfirmEmailTestCases() { }, ); - const incorrectCode = '12345'; // This code is incorrect + const incorrectCode = '123456'; // This code is incorrect const result = await axios.post( graphqlUrl, @@ -838,4 +845,55 @@ function userVerificationConfirmEmailTestCases() { assert.equal(result.data.errors[0].message, errorMessages.INCORRECT_CODE); }); + + it('should throw error when email confirmation code is expired for user email verification', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.email = 'test@example.com'; + user.emailConfirmed = false; + const userID = (await user.save()).id; + + const accessToken = await generateTestAccessToken(user.id); + await axios.post( + graphqlUrl, + { + query: userVerificationSendEmailConfirmation, + variables: { + userId: user.id, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Simulate expiration + await UserEmailVerification.update( + { user: { id: userID } }, + { emailVerificationCodeExpiredAt: new Date(Date.now() - 10000) }, + ); + + const emailVerificationFields = + await getUserEmailConfirmationFields(userID); + const expiredCode = emailVerificationFields?.emailVerificationCode; + + const result = await axios.post( + graphqlUrl, + { + query: userVerificationConfirmEmail, + variables: { + userId: user.id, + emailConfirmationCode: expiredCode, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + assert.equal(result.data.errors[0].message, errorMessages.CODE_EXPIRED); + }); } diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 8ebc3164e..ac71a97e3 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -18,6 +18,8 @@ import { validateEmail } from '../utils/validators/commonValidators'; import { findUserById, findUserByWalletAddress, + updateUserEmailConfirmationStatus, + getUserEmailConfirmationFields, } from '../repositories/userRepository'; import { createNewAccountVerification } from '../repositories/accountVerificationRepository'; import { UserByAddressResponse } from './types/userResolver'; @@ -178,12 +180,15 @@ export class UserResolver { throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_EMAIL)); } if (dbUser.email !== email) { - dbUser.emailConfirmed = false; - dbUser.emailConfirmationSent = false; - dbUser.emailConfirmationCode = null; - dbUser.emailConfirmationCodeExpiredAt = null; - dbUser.emailConfirmationSentAt = null; - dbUser.emailConfirmedAt = null; + await updateUserEmailConfirmationStatus({ + userId: dbUser.id, + emailConfirmed: false, + emailConfirmedAt: null, + emailVerificationCodeExpiredAt: null, + emailVerificationCode: null, + emailConfirmationSent: null, + emailConfirmationSentAt: null, + }); } dbUser.email = email; } @@ -272,14 +277,19 @@ export class UserResolver { const code = Math.floor(100000 + Math.random() * 900000).toString(); - userToVerify.emailConfirmationCodeExpiredAt = moment() + const emailVerificationCodeExpiredAt = moment() .add(5, 'minutes') .toDate(); - userToVerify.emailConfirmationCode = code; - userToVerify.emailConfirmationSent = true; - userToVerify.emailConfirmed = false; - userToVerify.emailConfirmationSentAt = new Date(); - await userToVerify.save(); + + await updateUserEmailConfirmationStatus({ + userId: userToVerify.id, + emailConfirmed: false, + emailConfirmedAt: null, + emailVerificationCodeExpiredAt, + emailVerificationCode: code, + emailConfirmationSent: true, + emailConfirmationSentAt: new Date(), + }); await getNotificationAdapter().sendUserEmailConfirmation({ email, @@ -302,7 +312,7 @@ export class UserResolver { ): Promise { try { const currentUserId = user?.userId; - if (!currentUserId || currentUserId != userId) { + if (!currentUserId || currentUserId !== userId) { throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); } @@ -312,15 +322,39 @@ export class UserResolver { throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); } - if (emailConfirmationCode !== userFromDB.emailConfirmationCode) { + const emailConfirmationFields = await getUserEmailConfirmationFields( + userFromDB.id, + ); + + if (!emailConfirmationFields) { + throw new Error( + i18n.__(translationErrorMessagesKeys.NO_EMAIL_VERIFICATION_DATA), + ); + } + + if ( + emailConfirmationCode !== emailConfirmationFields.emailVerificationCode + ) { throw new Error(i18n.__(translationErrorMessagesKeys.INCORRECT_CODE)); } - userFromDB.emailConfirmationCodeExpiredAt = null; - userFromDB.emailConfirmationCode = null; - userFromDB.emailConfirmedAt = new Date(); - userFromDB.emailConfirmed = true; - await userFromDB.save(); + const currentTime = new Date(); + if ( + emailConfirmationFields.emailVerificationCodeExpiredAt && + emailConfirmationFields.emailVerificationCodeExpiredAt < currentTime + ) { + throw new Error(i18n.__(translationErrorMessagesKeys.CODE_EXPIRED)); + } + + await updateUserEmailConfirmationStatus({ + userId: userFromDB.id, + emailConfirmed: true, + emailConfirmedAt: new Date(), + emailVerificationCodeExpiredAt: null, + emailVerificationCode: null, + emailConfirmationSent: null, + emailConfirmationSentAt: null, + }); return userFromDB; } catch (e) { diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 6f82d18b6..d61253b89 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -178,6 +178,8 @@ export const errorMessages = { NO_EMAIL_PROVIDED: 'No email address provided.', INCORRECT_CODE: 'The verification code you entered is incorrect.', + NO_EMAIL_VERIFICATION_DATA: 'No email verification data found', + CODE_EXPIRED: 'The verification code has expired. Please request a new code.', }; export const translationErrorMessagesKeys = { @@ -324,4 +326,6 @@ export const translationErrorMessagesKeys = { ABC_NOT_FOUND: 'ABC_NOT_FOUND', NO_EMAIL_PROVIDED: 'NO_EMAIL_PROVIDED', INCORRECT_CODE: 'INCORRECT_CODE', + NO_EMAIL_VERIFICATION_DATA: 'NO_EMAIL_VERIFICATION_DATA', + CODE_EXPIRED: 'CODE_EXPIRED', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index f6ef11b45..8ebbc0f08 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -103,5 +103,7 @@ "TX_NOT_FOUND": "TX_NOT_FOUND", "ABC_NOT_FOUND": "Abc not found", "NO_EMAIL_PROVIDED": "No email address provided.", - "INCORRECT_CODE": "The verification code you entered is incorrect." + "INCORRECT_CODE": "The verification code you entered is incorrect.", + "NO_EMAIL_VERIFICATION_DATA": "No email verification data found", + "CODE_EXPIRED": "The verification code has expired. Please request a new code." } diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index 9d44eec2e..3bb846d92 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -99,6 +99,8 @@ "DRAFT_DONATION_DISABLED": "El borrador de donación está deshabilitado", "EVM_SUPPORT_ONLY": "Solo se admite EVM", "ABC_NOT_FOUND": "ABC no encontrado", - "NO_EMAIL_PROVIDED": "No se ha proporcionado una dirección de correo electrónico." - "INCORRECT_CODE": "El código de verificación que ingresaste es incorrecto." + "NO_EMAIL_PROVIDED": "No se ha proporcionado una dirección de correo electrónico.", + "INCORRECT_CODE": "El código de verificación que ingresaste es incorrecto.", + "NO_EMAIL_VERIFICATION_DATA": "No se encontraron datos de verificación de correo electrónico.", + "CODE_EXPIRED": "El código de verificación ha expirado. Por favor, solicita un nuevo código." } From 54fd3d4f95eb87bce499e470799cb7cab4226e8d Mon Sep 17 00:00:00 2001 From: Amin Latifi Date: Mon, 19 Aug 2024 11:05:35 +0330 Subject: [PATCH 06/12] Added user email verification to the entities --- src/entities/entities.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entities/entities.ts b/src/entities/entities.ts index 2a02d5114..cae0aa133 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -31,11 +31,13 @@ import { ProjectFraud } from './projectFraud'; import { ProjectActualMatchingView } from './ProjectActualMatchingView'; import { ProjectSocialMedia } from './projectSocialMedia'; import { UserQfRoundModelScore } from './userQfRoundModelScore'; +import { UserEmailVerification } from './userEmailVerification'; export const getEntities = (): DataSourceOptions['entities'] => { return [ Organization, User, + UserEmailVerification, ReferredEvent, Project, From ba2a8c6ae9f31fd4d9fdbd2618e4d0895eda96d6 Mon Sep 17 00:00:00 2001 From: Amin Latifi Date: Mon, 19 Aug 2024 11:14:55 +0330 Subject: [PATCH 07/12] Updated types --- src/entities/user.ts | 4 ++-- src/entities/userEmailVerification.ts | 2 +- src/repositories/userRepository.ts | 2 +- src/resolvers/userResolver.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/entities/user.ts b/src/entities/user.ts index 061751721..fc2a22745 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -193,9 +193,9 @@ export class User extends BaseEntity { @Column({ default: false }) emailConfirmed: boolean; - @Field(_type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: false }) @Column({ default: false }) - emailConfirmationSent: boolean | null; + emailConfirmationSent: boolean; @Field(_type => Date, { nullable: true }) @Column({ type: 'timestamptz', nullable: true }) diff --git a/src/entities/userEmailVerification.ts b/src/entities/userEmailVerification.ts index 9c94d7bd0..d39b08da8 100644 --- a/src/entities/userEmailVerification.ts +++ b/src/entities/userEmailVerification.ts @@ -15,7 +15,7 @@ export class UserEmailVerification extends BaseEntity { @OneToOne(() => User, user => user.emailVerification, { onDelete: 'CASCADE' }) user: User; - @Column({ nullable: true }) + @Column('text', { nullable: true }) emailVerificationCode: string | null; @Column('timestamptz', { nullable: true }) diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 910eef312..73ac8fc3c 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -186,7 +186,7 @@ export const updateUserEmailConfirmationStatus = async (params: { emailConfirmedAt: Date | null; emailVerificationCodeExpiredAt: Date | null; emailVerificationCode: string | null; - emailConfirmationSent: boolean | null; + emailConfirmationSent: boolean; emailConfirmationSentAt: Date | null; }): Promise => { const { diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index ac71a97e3..bf453fba4 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -186,7 +186,7 @@ export class UserResolver { emailConfirmedAt: null, emailVerificationCodeExpiredAt: null, emailVerificationCode: null, - emailConfirmationSent: null, + emailConfirmationSent: false, emailConfirmationSentAt: null, }); } @@ -352,7 +352,7 @@ export class UserResolver { emailConfirmedAt: new Date(), emailVerificationCodeExpiredAt: null, emailVerificationCode: null, - emailConfirmationSent: null, + emailConfirmationSent: false, emailConfirmationSentAt: null, }); From 4b290fae9c7725d4f9ee02dfb7cfedddf0a0f1bf Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 15:30:24 +0330 Subject: [PATCH 08/12] Remove relation between user and userEmailVerification tables --- src/entities/user.ts | 7 ------- src/entities/userEmailVerification.ts | 12 +----------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/entities/user.ts b/src/entities/user.ts index fc2a22745..12312c2f2 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -18,7 +18,6 @@ import { ProjectStatusHistory } from './projectStatusHistory'; import { ProjectVerificationForm } from './projectVerificationForm'; import { ReferredEvent } from './referredEvent'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; -import { UserEmailVerification } from './userEmailVerification'; export const publicSelectionFields = [ 'user.id', @@ -205,12 +204,6 @@ export class User extends BaseEntity { @Column({ type: 'timestamptz', nullable: true }) emailConfirmedAt: Date | null; - @OneToOne( - () => UserEmailVerification, - emailVerification => emailVerification.user, - ) - emailVerification: UserEmailVerification; - @Field(_type => Int, { nullable: true }) async donationsCount() { return await Donation.createQueryBuilder('donation') diff --git a/src/entities/userEmailVerification.ts b/src/entities/userEmailVerification.ts index d39b08da8..784f03a56 100644 --- a/src/entities/userEmailVerification.ts +++ b/src/entities/userEmailVerification.ts @@ -1,20 +1,10 @@ -import { - Entity, - PrimaryGeneratedColumn, - Column, - OneToOne, - BaseEntity, -} from 'typeorm'; -import { User } from './user'; +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'; @Entity() export class UserEmailVerification extends BaseEntity { @PrimaryGeneratedColumn() id: number; - @OneToOne(() => User, user => user.emailVerification, { onDelete: 'CASCADE' }) - user: User; - @Column('text', { nullable: true }) emailVerificationCode: string | null; From b0166278e32888db3dab95f47498a025332fab01 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 15:53:30 +0330 Subject: [PATCH 09/12] Add missing userId to userEmailVerification entity --- src/entities/userEmailVerification.ts | 3 +++ src/repositories/userRepository.test.ts | 12 ++++++------ src/repositories/userRepository.ts | 6 +++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/entities/userEmailVerification.ts b/src/entities/userEmailVerification.ts index 784f03a56..31081cde8 100644 --- a/src/entities/userEmailVerification.ts +++ b/src/entities/userEmailVerification.ts @@ -5,6 +5,9 @@ export class UserEmailVerification extends BaseEntity { @PrimaryGeneratedColumn() id: number; + @Column() + userId: number; + @Column('text', { nullable: true }) emailVerificationCode: string | null; diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index 63d07e245..73331446e 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -511,7 +511,7 @@ function updateUserEmailConfirmationStatusTestCases() { }).save(); await UserEmailVerification.create({ - user: user, + userId: user.id, emailVerificationCode: '234567', emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), }).save(); @@ -528,7 +528,7 @@ function updateUserEmailConfirmationStatusTestCases() { // Verify changes in UserEmailVerification table const updatedVerification = await UserEmailVerification.findOne({ - where: { user: { id: user.id } }, + where: { userId: user.id }, }); assert.isNotNull(updatedVerification); @@ -563,7 +563,7 @@ function updateUserEmailConfirmationStatusTestCases() { // Verify new entry in UserEmailVerification table const newVerification = await UserEmailVerification.findOne({ - where: { user: { id: user.id } }, + where: { userId: user.id }, }); assert.isNotNull(newVerification); @@ -604,7 +604,7 @@ function getUserEmailConfirmationFieldsTestCases() { }).save(); const emailVerification = await UserEmailVerification.create({ - user: user, + userId: user.id, emailVerificationCode: '123456', emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), // 1 hour from now }).save(); @@ -652,7 +652,7 @@ function getUserEmailConfirmationFieldsTestCases() { }).save(); const emailVerification = await UserEmailVerification.create({ - user: user, + userId: user.id, emailVerificationCode: '654321', emailVerificationCodeExpiredAt: new Date(Date.now() + 7200 * 1000), // 2 hours from now }).save(); @@ -681,7 +681,7 @@ function getUserEmailConfirmationFieldsTestCases() { }).save(); const emailVerification = await UserEmailVerification.create({ - user: user, + userId: user.id, emailVerificationCode: '111111', emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), // 1 hour from now }).save(); diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 73ac8fc3c..59bbc8e19 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -200,12 +200,12 @@ export const updateUserEmailConfirmationStatus = async (params: { } = params; let userVerification = await UserEmailVerification.findOne({ - where: { user: { id: userId } }, + where: { userId }, }); if (!userVerification) { userVerification = new UserEmailVerification(); - userVerification.user = { id: userId } as User; // Creating a new association with the user + userVerification.userId = userId; } userVerification.emailVerificationCode = emailVerificationCode; @@ -232,7 +232,7 @@ export const getUserEmailConfirmationFields = async ( ): Promise => { // Find the email verification entry for the given user ID const emailVerification = await UserEmailVerification.findOne({ - where: { user: { id: userId } }, + where: { userId }, }); return emailVerification || null; From 68b92e6c7cdb2f13980bf55eabaf9878c688fa56 Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 15:59:18 +0330 Subject: [PATCH 10/12] Add missing userId to userResolver.test.ts --- src/resolvers/userResolver.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index d6bdf05ce..6c57cc616 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -870,7 +870,7 @@ function userVerificationConfirmEmailTestCases() { // Simulate expiration await UserEmailVerification.update( - { user: { id: userID } }, + { userId: userID }, { emailVerificationCodeExpiredAt: new Date(Date.now() - 10000) }, ); From aaece02b3d698e398fc27758ce153f805a0857ce Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 16:38:04 +0330 Subject: [PATCH 11/12] Fix userRepository.test.ts --- src/repositories/userRepository.test.ts | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index 73331446e..2f2b2c3db 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -603,10 +603,12 @@ function getUserEmailConfirmationFieldsTestCases() { emailConfirmationSentAt: new Date(), }).save(); - const emailVerification = await UserEmailVerification.create({ + const expirationTime = new Date(Date.now() + 3600 * 1000); // 1 hour from now + + await UserEmailVerification.create({ userId: user.id, emailVerificationCode: '123456', - emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), // 1 hour from now + emailVerificationCodeExpiredAt: expirationTime, }).save(); const result = await getUserEmailConfirmationFields(user.id); @@ -615,7 +617,7 @@ function getUserEmailConfirmationFieldsTestCases() { assert.equal(result?.emailVerificationCode, '123456'); assert.equal( result?.emailVerificationCodeExpiredAt!.getTime(), - emailVerification.emailVerificationCodeExpiredAt!.getTime(), + expirationTime.getTime(), ); assert.equal(user.emailConfirmationSent, true); assert.equal( @@ -638,7 +640,7 @@ function getUserEmailConfirmationFieldsTestCases() { }); it('should return null if the user ID does not exist', async () => { - const result = await getUserEmailConfirmationFields(999); // non-existent user ID + const result = await getUserEmailConfirmationFields(999999); // non-existent user ID assert.isNull(result); }); @@ -651,10 +653,11 @@ function getUserEmailConfirmationFieldsTestCases() { emailConfirmationSentAt: new Date(Date.now() - 3600 * 1000), // 1 hour ago }).save(); - const emailVerification = await UserEmailVerification.create({ + const expirationTime = new Date(Date.now() + 7200 * 1000); // 2 hours from now + await UserEmailVerification.create({ userId: user.id, emailVerificationCode: '654321', - emailVerificationCodeExpiredAt: new Date(Date.now() + 7200 * 1000), // 2 hours from now + emailVerificationCodeExpiredAt: expirationTime, }).save(); const result = await getUserEmailConfirmationFields(user.id); @@ -663,7 +666,7 @@ function getUserEmailConfirmationFieldsTestCases() { assert.equal(result?.emailVerificationCode, '654321'); assert.equal( result?.emailVerificationCodeExpiredAt!.getTime(), - emailVerification.emailVerificationCodeExpiredAt!.getTime(), + expirationTime.getTime(), ); assert.equal(user.emailConfirmationSent, true); assert.equal( @@ -680,10 +683,11 @@ function getUserEmailConfirmationFieldsTestCases() { emailConfirmationSentAt: new Date(), }).save(); - const emailVerification = await UserEmailVerification.create({ + const expirationTime = new Date(Date.now() + 3600 * 1000); // 1 hour from now + await UserEmailVerification.create({ userId: user.id, emailVerificationCode: '111111', - emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), // 1 hour from now + emailVerificationCodeExpiredAt: expirationTime, }).save(); const result = await getUserEmailConfirmationFields(user.id); @@ -692,7 +696,7 @@ function getUserEmailConfirmationFieldsTestCases() { assert.equal(result?.emailVerificationCode, '111111'); assert.equal( result?.emailVerificationCodeExpiredAt!.getTime(), - emailVerification.emailVerificationCodeExpiredAt!.getTime(), + expirationTime.getTime(), ); assert.equal(user.emailConfirmationSent, false); assert.equal( From 10fa30ec0015afb4500e48160492807d910bae4c Mon Sep 17 00:00:00 2001 From: ali ebrahimi Date: Mon, 19 Aug 2024 17:07:44 +0330 Subject: [PATCH 12/12] Fix bug in userResolver that is related to return non-updated data for user --- src/resolvers/userResolver.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index bf453fba4..508b38c53 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -291,13 +291,19 @@ export class UserResolver { emailConfirmationSentAt: new Date(), }); + const updatedUser = await findUserById(userId); + + if (!updatedUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + } + await getNotificationAdapter().sendUserEmailConfirmation({ email, - user: userToVerify, + user: updatedUser, code, }); - return userToVerify; + return updatedUser; } catch (e) { logger.error('userVerificationSendEmailConfirmation() error', e); throw e; @@ -356,7 +362,13 @@ export class UserResolver { emailConfirmationSentAt: null, }); - return userFromDB; + const updatedUser = await findUserById(userId); + + if (!updatedUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + } + + return updatedUser; } catch (e) { logger.error('userVerificationConfirmEmail() error', e); throw e;