From 9da2047e8c0899b3210bc7e255d5e3dbb5da00cc Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Mar 2022 19:14:59 +0100 Subject: [PATCH] move sendActivationEmail mutation from user to admin resolver --- backend/src/graphql/resolver/AdminResolver.ts | 36 ++++++ backend/src/graphql/resolver/UserResolver.ts | 106 ++++-------------- 2 files changed, 57 insertions(+), 85 deletions(-) diff --git a/backend/src/graphql/resolver/AdminResolver.ts b/backend/src/graphql/resolver/AdminResolver.ts index d98b38b7f..061c485c5 100644 --- a/backend/src/graphql/resolver/AdminResolver.ts +++ b/backend/src/graphql/resolver/AdminResolver.ts @@ -34,6 +34,8 @@ import { Decay } from '@model/Decay' import Paginated from '@arg/Paginated' import { Order } from '@enum/Order' import { communityUser } from '@/util/communityUser' +import { checkExistingOptInCode, activationLink } from './UserResolver' +import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' // const EMAIL_OPT_IN_REGISTER = 1 // const EMAIL_OPT_UNKNOWN = 3 // elopage? @@ -369,6 +371,40 @@ export class AdminResolver { const user = await dbUser.findOneOrFail({ id: userId }) return userTransactions.map((t) => new Transaction(t, new User(user), communityUser)) } + + @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) + @Mutation(() => Boolean) + async sendActivationEmail(@Arg('email') email: string): Promise { + email = email.trim().toLowerCase() + const user = await dbUser.findOneOrFail({ email: email }) + + // can be both types: REGISTER and RESET_PASSWORD + let optInCode = await LoginEmailOptIn.findOne({ + userId: user.id, + }) + + optInCode = checkExistingOptInCode(optInCode, user.id) + // keep the optin type (when newly created is is REGISTER) + await LoginEmailOptIn.save(optInCode) + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const emailSent = await sendAccountActivationEmail({ + link: activationLink(optInCode), + firstName: user.firstName, + lastName: user.lastName, + email, + }) + + /* uncomment this, when you need the activation link on the console + // In case EMails are disabled log the activation link for the user + if (!emailSent) { + // eslint-disable-next-line no-console + console.log(`Account confirmation link: ${activationLink}`) + } + */ + + return true + } } interface CreationMap { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 9f48049e1..7cce31eab 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } from 'type-graphql' -import { getConnection, getCustomRepository, QueryRunner } from '@dbTools/typeorm' +import { getConnection, getCustomRepository } from '@dbTools/typeorm' import CONFIG from '@/config' import { User } from '@model/User' import { User as DbUser } from '@entity/User' @@ -155,35 +155,29 @@ const newEmailOptIn = (userId: number): LoginEmailOptIn => { return emailOptIn } -const createEmailOptIn = async ( +// needed by AdminResolver +// checks if given code exists and can be resent +// if optIn does not exits, it is created +export const checkExistingOptInCode = ( + optInCode: LoginEmailOptIn | undefined, userId: number, - queryRunner: QueryRunner, -): Promise => { - let emailOptIn = await LoginEmailOptIn.findOne({ - userId, - emailOptInTypeId: OptInType.EMAIL_OPT_IN_REGISTER, - }) - - if (emailOptIn) { - if (isOptInValid(emailOptIn)) { +): LoginEmailOptIn => { + if (optInCode) { + if (!canResendOptIn(optInCode)) { throw new Error( - `email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`, + `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, ) } - emailOptIn.updatedAt = new Date() - emailOptIn.resendCount++ + optInCode.updatedAt = new Date() + optInCode.resendCount++ } else { - emailOptIn = new LoginEmailOptIn() - emailOptIn.verificationCode = random(64) - emailOptIn.userId = userId - emailOptIn.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER + optInCode = newEmailOptIn(userId) } - await queryRunner.manager.save(emailOptIn).catch((error) => { - // eslint-disable-next-line no-console - console.log('Error while saving emailOptIn', error) - throw new Error('error saving email opt in') - }) - return emailOptIn + return optInCode +} + +export const activationLink = (optInCode: LoginEmailOptIn): string => { + return CONFIG.EMAIL_LINK_SETPASSWORD.replace(/{optin}/g, optInCode.verificationCode.toString()) } @Resolver() @@ -388,50 +382,6 @@ export class UserResolver { return new User(dbUser) } - // This is used by the admin only - should we move it to the admin resolver? - @Authorized([RIGHTS.SEND_ACTIVATION_EMAIL]) - @Mutation(() => Boolean) - async sendActivationEmail(@Arg('email') email: string): Promise { - email = email.trim().toLowerCase() - const user = await DbUser.findOneOrFail({ email: email }) - - const queryRunner = getConnection().createQueryRunner() - await queryRunner.connect() - await queryRunner.startTransaction('READ UNCOMMITTED') - - try { - const emailOptIn = await createEmailOptIn(user.id, queryRunner) - - const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace( - /{optin}/g, - emailOptIn.verificationCode.toString(), - ) - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const emailSent = await sendAccountActivationEmail({ - link: activationLink, - firstName: user.firstName, - lastName: user.lastName, - email, - }) - - /* uncomment this, when you need the activation link on the console - // In case EMails are disabled log the activation link for the user - if (!emailSent) { - // eslint-disable-next-line no-console - console.log(`Account confirmation link: ${activationLink}`) - } - */ - await queryRunner.commitTransaction() - } catch (e) { - await queryRunner.rollbackTransaction() - throw e - } finally { - await queryRunner.release() - } - return true - } - @Authorized([RIGHTS.SEND_RESET_PASSWORD_EMAIL]) @Query(() => Boolean) async sendResetPasswordEmail(@Arg('email') email: string): Promise { @@ -443,29 +393,14 @@ export class UserResolver { userId: user.id, }) - if (optInCode) { - if (!canResendOptIn(optInCode)) { - throw new Error( - `email already sent less than $(printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} minutes ago`, - ) - } - optInCode.updatedAt = new Date() - optInCode.resendCount++ - } else { - optInCode = newEmailOptIn(user.id) - } + optInCode = checkExistingOptInCode(optInCode, user.id) // now it is RESET_PASSWORD optInCode.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD await LoginEmailOptIn.save(optInCode) - const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( - /{optin}/g, - optInCode.verificationCode.toString(), - ) - // eslint-disable-next-line @typescript-eslint/no-unused-vars const emailSent = await sendResetPasswordEmail({ - link, + link: activationLink(optInCode), firstName: user.firstName, lastName: user.lastName, email, @@ -547,6 +482,7 @@ export class UserResolver { throw new Error('error saving user: ' + error) }) + // why do we delete the code? // Delete Code await queryRunner.manager.remove(optInCode).catch((error) => { throw new Error('error deleting code: ' + error)