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/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 798a5a79a..d975a6ed7 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -100,16 +100,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/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, diff --git a/src/entities/user.ts b/src/entities/user.ts index 6fae47a1d..12312c2f2 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -192,15 +192,7 @@ export class User extends BaseEntity { @Column({ default: false }) emailConfirmed: boolean; - @Field(_type => String, { nullable: true }) - @Column('text', { nullable: true }) - emailConfirmationToken: string | null; - - @Field(_type => Date, { nullable: true }) - @Column('timestamptz', { nullable: true }) - emailConfirmationTokenExpiredAt: Date | null; - - @Field(_type => Boolean, { nullable: true }) + @Field(_type => Boolean, { nullable: false }) @Column({ default: false }) emailConfirmationSent: boolean; diff --git a/src/entities/userEmailVerification.ts b/src/entities/userEmailVerification.ts new file mode 100644 index 000000000..31081cde8 --- /dev/null +++ b/src/entities/userEmailVerification.ts @@ -0,0 +1,16 @@ +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'; + +@Entity() +export class UserEmailVerification extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column() + userId: number; + + @Column('text', { 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 4b482d91b..2f2b2c3db 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -11,16 +11,16 @@ import { User, UserRole } from '../entities/user'; import { findAdminUserByEmail, findAllUsers, - findUserByEmailConfirmationToken, findUserById, findUserByWalletAddress, findUsersWhoDonatedToProjectExcludeWhoLiked, findUsersWhoLikedProjectExcludeProjectOwner, findUsersWhoSupportProject, + getUserEmailConfirmationFields, updateUserEmailConfirmationStatus, - updateUserEmailConfirmationToken, } from './userRepository'; import { Reaction } from '../entities/reaction'; +import { UserEmailVerification } from '../entities/userEmailVerification'; describe('sql injection test cases', sqlInjectionTestCases); @@ -48,16 +48,12 @@ describe( ); describe( - 'userRepository.findUserByEmailConfirmationToken', - findUserByEmailConfirmationTokenTestCases, -); -describe( - 'userRepository.updateUserEmailConfirmationStatus', + 'updateUserEmailConfirmationStatus() test cases', updateUserEmailConfirmationStatusTestCases, ); describe( - 'userRepository.updateUserEmailConfirmationToken', - updateUserEmailConfirmationTokenTestCases, + 'getUserEmailConfirmationFields() test cases', + getUserEmailConfirmationFieldsTestCases, ); function findUsersWhoDonatedToProjectTestCases() { @@ -506,106 +502,206 @@ function findUsersWhoSupportProjectTestCases() { }); } -function findUserByEmailConfirmationTokenTestCases() { - it('should return a user if a valid email confirmation token is provided', async () => { - await User.create({ +function updateUserEmailConfirmationStatusTestCases() { + it('should update the email confirmation status of a user', async () => { + const user = await User.create({ email: 'test@example.com', - emailConfirmationToken: 'validToken123', + emailConfirmed: false, loginType: 'wallet', }).save(); - const foundUser = await findUserByEmailConfirmationToken('validToken123'); - assert.isNotNull(foundUser); - assert.equal(foundUser!.email, 'test@example.com'); - assert.equal(foundUser!.emailConfirmationToken, 'validToken123'); - }); + await UserEmailVerification.create({ + userId: user.id, + emailVerificationCode: '234567', + emailVerificationCodeExpiredAt: new Date(Date.now() + 3600 * 1000), + }).save(); - it('should return null if no user is found with the provided email confirmation token', async () => { - const foundUser = await findUserByEmailConfirmationToken('invalidToken123'); - assert.isNull(foundUser); + 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 changes in UserEmailVerification table + const updatedVerification = await UserEmailVerification.findOne({ + where: { userId: user.id }, + }); + + 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.isNotNull(updatedUser!.emailConfirmedAt); + assert.isFalse(updatedUser!.emailConfirmationSent); + assert.isNull(updatedUser!.emailConfirmationSentAt); }); -} -function updateUserEmailConfirmationStatusTestCases() { - it('should update the email confirmation status of a user', async () => { + 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: 'test@example.com', + email: 'test2@example.com', emailConfirmed: false, - emailConfirmationToken: 'validToken123', loginType: 'wallet', }).save(); await updateUserEmailConfirmationStatus({ userId: user.id, emailConfirmed: true, - emailConfirmationTokenExpiredAt: null, - emailConfirmationToken: null, - emailConfirmationSentAt: null, + 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: { userId: user.id }, }); - // Using findOne with options object + 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.isNull(updatedUser!.emailConfirmationToken); + 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, - emailConfirmationTokenExpiredAt: null, - emailConfirmationToken: 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 updateUserEmailConfirmationTokenTestCases() { - it('should update the email confirmation token 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 newToken = 'newToken123'; - const newExpiryDate = new Date(Date.now() + 3600 * 1000); // 1 hour from now - const sentAtDate = new Date(); + const expirationTime = new Date(Date.now() + 3600 * 1000); // 1 hour from now - await updateUserEmailConfirmationToken({ + await UserEmailVerification.create({ userId: user.id, - emailConfirmationToken: newToken, - emailConfirmationTokenExpiredAt: newExpiryDate, - emailConfirmationSentAt: sentAtDate, - }); + emailVerificationCode: '123456', + emailVerificationCodeExpiredAt: expirationTime, + }).save(); - // Using findOne with options object - const updatedUser = await User.findOne({ where: { id: user.id } }); - assert.isNotNull(updatedUser); - assert.equal(updatedUser!.emailConfirmationToken, newToken); + const result = await getUserEmailConfirmationFields(user.id); + + assert.isNotNull(result); + assert.equal(result?.emailVerificationCode, '123456'); assert.equal( - updatedUser!.emailConfirmationTokenExpiredAt!.getTime(), - newExpiryDate.getTime(), + result?.emailVerificationCodeExpiredAt!.getTime(), + expirationTime.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 updateUserEmailConfirmationToken({ - userId: 999, // non-existent userId - emailConfirmationToken: 'newToken123', - emailConfirmationTokenExpiredAt: 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(999999); // 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 expirationTime = new Date(Date.now() + 7200 * 1000); // 2 hours from now + await UserEmailVerification.create({ + userId: user.id, + emailVerificationCode: '654321', + emailVerificationCodeExpiredAt: expirationTime, + }).save(); + + const result = await getUserEmailConfirmationFields(user.id); + + assert.isNotNull(result); + assert.equal(result?.emailVerificationCode, '654321'); + assert.equal( + result?.emailVerificationCodeExpiredAt!.getTime(), + expirationTime.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 expirationTime = new Date(Date.now() + 3600 * 1000); // 1 hour from now + await UserEmailVerification.create({ + userId: user.id, + emailVerificationCode: '111111', + emailVerificationCodeExpiredAt: expirationTime, + }).save(); + + const result = await getUserEmailConfirmationFields(user.id); + + assert.isNotNull(result); + assert.equal(result?.emailVerificationCode, '111111'); + assert.equal( + result?.emailVerificationCodeExpiredAt!.getTime(), + expirationTime.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 985926596..59bbc8e19 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, @@ -179,66 +180,60 @@ 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; + emailConfirmedAt: Date | null; + emailVerificationCodeExpiredAt: Date | null; + emailVerificationCode: string | null; + emailConfirmationSent: boolean; emailConfirmationSentAt: Date | null; }): Promise => { const { userId, emailConfirmed, - emailConfirmationTokenExpiredAt, - emailConfirmationToken, + emailConfirmedAt, + emailVerificationCodeExpiredAt, + emailVerificationCode, + emailConfirmationSent, emailConfirmationSentAt, } = params; + let userVerification = await UserEmailVerification.findOne({ + where: { userId }, + }); + + if (!userVerification) { + userVerification = new UserEmailVerification(); + userVerification.userId = userId; + } + + 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, - emailConfirmationTokenExpiredAt, - emailConfirmationToken, + emailConfirmedAt, + emailConfirmationSent, emailConfirmationSentAt, }) .where('id = :userId', { userId }) .execute(); }; -export const updateUserEmailConfirmationToken = async (params: { - userId: number; - emailConfirmationToken: string; - emailConfirmationTokenExpiredAt: Date; - emailConfirmationSentAt: Date; -}): Promise => { - const { - userId, - emailConfirmationToken, - emailConfirmationTokenExpiredAt, - emailConfirmationSentAt, - } = params; - - const user = await findUserById(userId); - if (!user) { - throw new Error('User not found'); - } - - user.emailConfirmationToken = emailConfirmationToken; - user.emailConfirmationTokenExpiredAt = emailConfirmationTokenExpiredAt; - 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: { userId }, + }); - await user.save(); - return user; + return emailVerification || null; }; diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index 38772227a..6c57cc616 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,8 @@ 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'; +import { getUserEmailConfirmationFields } from '../repositories/userRepository'; +import { UserEmailVerification } from '../entities/userEmailVerification'; describe('updateUser() test cases', updateUserTestCases); describe('userByAddress() test cases', userByAddressTestCases); @@ -708,10 +707,13 @@ function userVerificationSendEmailConfirmationTestCases() { .emailConfirmationSent, true, ); - assert.isNotNull( - result.data.data.userVerificationSendEmailConfirmation - .emailConfirmationToken, + + 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 () => { @@ -748,10 +750,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, @@ -766,16 +768,17 @@ function userVerificationConfirmEmailTestCases() { }, ); - const token = - emailConfirmationSentResult.data.data - .userVerificationSendEmailConfirmation.emailConfirmationToken; + const emailVerificationFields = + await getUserEmailConfirmationFields(userID); + const code = emailVerificationFields?.emailVerificationCode; const result = await axios.post( graphqlUrl, { query: userVerificationConfirmEmail, variables: { - emailConfirmationToken: token, + userId: user.id, + emailConfirmationCode: code, }, }, { @@ -793,26 +796,44 @@ 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 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 = '123456'; // This code is incorrect const result = await axios.post( graphqlUrl, { query: userVerificationConfirmEmail, variables: { - emailConfirmationToken: token, + userId: user.id, + emailConfirmationCode: incorrectCode, }, }, { @@ -822,12 +843,57 @@ function userVerificationConfirmEmailTestCases() { }, ); - assert.equal(result.data.errors[0].message, 'jwt expired'); - const userReinitializedEmailParams = await findUserById(user.id); + 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( + { userId: 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.isFalse(userReinitializedEmailParams!.emailConfirmed); - assert.isFalse(userReinitializedEmailParams!.emailConfirmationSent); - assert.isNotOk(userReinitializedEmailParams!.emailConfirmationSentAt); - assert.isNull(userReinitializedEmailParams!.emailConfirmationToken); + assert.equal(result.data.errors[0].message, errorMessages.CODE_EXPIRED); }); } diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index a6150ab55..508b38c53 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -8,19 +8,18 @@ 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, + updateUserEmailConfirmationStatus, + getUserEmailConfirmationFields, } from '../repositories/userRepository'; import { createNewAccountVerification } from '../repositories/accountVerificationRepository'; import { UserByAddressResponse } from './types/userResolver'; @@ -181,12 +180,15 @@ export class UserResolver { throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_EMAIL)); } if (dbUser.email !== email) { - dbUser.emailConfirmed = false; - dbUser.emailConfirmationSent = false; - dbUser.emailConfirmationToken = null; - dbUser.emailConfirmationTokenExpiredAt = null; - dbUser.emailConfirmationSentAt = null; - dbUser.emailConfirmedAt = null; + await updateUserEmailConfirmationStatus({ + userId: dbUser.id, + emailConfirmed: false, + emailConfirmedAt: null, + emailVerificationCodeExpiredAt: null, + emailVerificationCode: null, + emailConfirmationSent: false, + emailConfirmationSentAt: null, + }); } dbUser.email = email; } @@ -273,28 +275,35 @@ 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() + const emailVerificationCodeExpiredAt = moment() .add(5, 'minutes') .toDate(); - userToVerify.emailConfirmationToken = token; - 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(), + }); + + const updatedUser = await findUserById(userId); + + if (!updatedUser) { + throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + } await getNotificationAdapter().sendUserEmailConfirmation({ email, - user: userToVerify, - token, + user: updatedUser, + code, }); - return userToVerify; + return updatedUser; } catch (e) { logger.error('userVerificationSendEmailConfirmation() error', e); throw e; @@ -303,50 +312,64 @@ 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 currentUserId = user?.userId; + if (!currentUserId || currentUserId !== userId) { + throw new Error(i18n.__(translationErrorMessagesKeys.UN_AUTHORIZED)); + } - const isValidToken = await findUserByEmailConfirmationToken( - emailConfirmationToken, - ); + const userFromDB = await findUserById(userId); - if (!isValidToken) { + if (!userFromDB) { throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); } - const decodedJwt: any = jwt.verify(emailConfirmationToken, secret); - const userId = decodedJwt.userId; - const user = await findUserById(userId); + const emailConfirmationFields = await getUserEmailConfirmationFields( + userFromDB.id, + ); - if (!user) { - throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + if (!emailConfirmationFields) { + throw new Error( + i18n.__(translationErrorMessagesKeys.NO_EMAIL_VERIFICATION_DATA), + ); } - user.emailConfirmationTokenExpiredAt = null; - user.emailConfirmationToken = null; - user.emailConfirmedAt = new Date(); - user.emailConfirmed = true; - await user.save(); + if ( + emailConfirmationCode !== emailConfirmationFields.emailVerificationCode + ) { + throw new Error(i18n.__(translationErrorMessagesKeys.INCORRECT_CODE)); + } - return user; - } catch (e) { - const user = await findUserByEmailConfirmationToken( - emailConfirmationToken, - ); + 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: false, + emailConfirmationSentAt: null, + }); - if (!user) { + const updatedUser = await findUserById(userId); + + if (!updatedUser) { throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); } - user.emailConfirmed = false; - user.emailConfirmationTokenExpiredAt = null; - user.emailConfirmationSent = false; - user.emailConfirmationSentAt = null; - user.emailConfirmationToken = null; - - await user.save(); + return updatedUser; + } catch (e) { logger.error('userVerificationConfirmEmail() error', e); throw e; } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index a8f8dfa13..d61253b89 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -175,6 +175,11 @@ 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.', + NO_EMAIL_VERIFICATION_DATA: 'No email verification data found', + CODE_EXPIRED: 'The verification code has expired. Please request a new code.', }; export const translationErrorMessagesKeys = { @@ -320,4 +325,7 @@ export const translationErrorMessagesKeys = { EVM_SUPPORT_ONLY: 'EVM_SUPPORT_ONLY', 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 ca706cc5f..8ebbc0f08 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -102,5 +102,8 @@ "INVALID_PROJECT_ID": "INVALID_PROJECT_ID", "TX_NOT_FOUND": "TX_NOT_FOUND", "ABC_NOT_FOUND": "Abc 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_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 468f99d42..3bb846d92 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -99,5 +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." + "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." } diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index de77ba21f..9cd78c158 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1996,8 +1996,6 @@ export const userVerificationSendEmailConfirmation = ` id email emailConfirmed - emailConfirmationToken - emailConfirmationTokenExpiredAt emailConfirmationSent emailConfirmationSentAt emailConfirmedAt @@ -2006,13 +2004,17 @@ 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 emailConfirmationSent emailConfirmationSentAt emailConfirmedAt