diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 7a55de8e8..820261bb1 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -53,6 +53,8 @@ const email = { EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '587', EMAIL_LINK_VERIFICATION: process.env.EMAIL_LINK_VERIFICATION || 'http://localhost/vue/checkEmail/$1', + EMAIL_LINK_SETPASSWORD: + process.env.EMAIL_LINK_SETPASSWORD || 'http://localhost/vue/setPassword/$1', } // This is needed by graphql-directive-auth diff --git a/backend/src/graphql/model/SendPasswordResetEmailResponse.ts b/backend/src/graphql/model/SendPasswordResetEmailResponse.ts deleted file mode 100644 index d387efede..000000000 --- a/backend/src/graphql/model/SendPasswordResetEmailResponse.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { ObjectType, Field } from 'type-graphql' - -@ObjectType() -export class SendPasswordResetEmailResponse { - constructor(json: any) { - this.state = json.state - this.msg = json.msg - } - - @Field(() => String) - state: string - - @Field(() => String) - msg?: string -} diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 25f83bb09..ffa78e8aa 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -6,7 +6,6 @@ import { Resolver, Query, Args, Arg, Authorized, Ctx, UseMiddleware, Mutation } import { getConnection, getCustomRepository } from 'typeorm' import CONFIG from '../../config' import { LoginViaVerificationCode } from '../model/LoginViaVerificationCode' -import { SendPasswordResetEmailResponse } from '../model/SendPasswordResetEmailResponse' import { User } from '../model/User' import { User as DbUser } from '@entity/User' import encode from '../../jwt/encode' @@ -30,6 +29,7 @@ import { LoginUserBackup } from '@entity/LoginUserBackup' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { sendEMail } from '../../util/sendEMail' import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys' +import { randomBytes } from 'crypto' // eslint-disable-next-line @typescript-eslint/no-var-requires const sodium = require('sodium-native') @@ -201,8 +201,6 @@ export class UserResolver { @Ctx() context: any, ): Promise { email = email.trim().toLowerCase() - // const result = await apiPost(CONFIG.LOGIN_API_URL + 'unsecureLogin', { email, password }) - // UnsecureLogin const loginUserRepository = getCustomRepository(LoginUserRepository) const loginUser = await loginUserRepository.findByEmail(email).catch(() => { throw new Error('No user with this credentials') @@ -440,20 +438,60 @@ export class UserResolver { return 'success' } - @Query(() => SendPasswordResetEmailResponse) - async sendResetPasswordEmail( - @Arg('email') email: string, - ): Promise { - const payload = { - email, - email_text: 7, - email_verification_code_type: 'resetPassword', + @Query(() => Boolean) + async sendResetPasswordEmail(@Arg('email') email: string): Promise { + let emailAlreadySend = false + const EMAIL_OPT_IN_RESET_PASSWORD = 2 + + const loginUser = await LoginUser.findOneOrFail({ email }) + + let optInCode = await LoginEmailOptIn.findOne({ + userId: loginUser.id, + emailOptInTypeId: EMAIL_OPT_IN_RESET_PASSWORD, + }) + if (optInCode) { + emailAlreadySend = true + } else { + optInCode = new LoginEmailOptIn() + optInCode.verificationCode = randomBytes(16).readBigInt64LE() + optInCode.userId = loginUser.id + optInCode.emailOptInTypeId = EMAIL_OPT_IN_RESET_PASSWORD + await optInCode.save() } - const response = await apiPost(CONFIG.LOGIN_API_URL + 'sendEmail', payload) - if (!response.success) { - throw new Error(response.data) + + const link = CONFIG.EMAIL_LINK_SETPASSWORD.replace( + /\$1/g, + optInCode.verificationCode.toString(), + ) + + if (emailAlreadySend) { + const timeElapsed = Date.now() - new Date(optInCode.updatedAt).getTime() + if (timeElapsed < 10 * 60 * 1000) { + throw new Error('email already sent less than a 10 minutes before') + } } - return new SendPasswordResetEmailResponse(response.data) + + const emailSent = await sendEMail({ + from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + to: `${loginUser.firstName} ${loginUser.lastName} <${email}>`, + subject: 'Gradido: Reset Password', + text: `Hallo ${loginUser.firstName} ${loginUser.lastName}, + + Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert. + Wenn du es warst, klicke bitte auf den Link: ${link} + oder kopiere den obigen Link in Dein Browserfenster. + + Mit freundlichen Grüßen, + dein Gradido-Team`, + }) + + // In case EMails are disabled log the activation link for the user + if (!emailSent) { + // eslint-disable-next-line no-console + console.log(`Reset password link: ${link}`) + } + + return true } @Mutation(() => String) diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 01021f601..bf4d07253 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -67,9 +67,7 @@ export const transactionsQuery = gql` export const sendResetPasswordEmail = gql` query($email: String!) { - sendResetPasswordEmail(email: $email) { - state - } + sendResetPasswordEmail(email: $email) } `