From 6699ec834d8f29757c002cce631f1204c190f459 Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Fri, 22 Jul 2022 16:38:21 +0800 Subject: [PATCH] add router handler for reset password --- packages/api/src/apollo.ts | 11 +- packages/api/src/routers/auth/auth_router.ts | 104 +++++++++++++++---- packages/api/src/services/create_user.ts | 38 +------ packages/api/src/services/send_emails.ts | 37 +++++++ packages/api/src/utils/auth.ts | 20 +++- packages/api/test/routers/auth.test.ts | 18 ++-- 6 files changed, 152 insertions(+), 76 deletions(-) create mode 100644 packages/api/src/services/send_emails.ts diff --git a/packages/api/src/apollo.ts b/packages/api/src/apollo.ts index c55ee5ecf..075245092 100644 --- a/packages/api/src/apollo.ts +++ b/packages/api/src/apollo.ts @@ -24,7 +24,7 @@ import ScalarResolvers from './scalars' import * as Sentry from '@sentry/node' import { createPubSubClient } from './datalayer/pubsub' import { initModels } from './server' -import { getClaimsByToken } from './utils/auth' +import { getClaimsByToken, setAuthInCookie } from './utils/auth' const signToken = promisify(jwt.sign) const logger = buildLogger('app.dispatch') @@ -76,14 +76,7 @@ const contextFunc: ContextFunction = async ({ setAuth: async ( claims: ClaimsToSet, secret: string = env.server.jwtSecret - ) => { - const token = await signToken(claims, secret) - - res.cookie('auth', token, { - httpOnly: true, - expires: new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000), - }) - }, + ) => await setAuthInCookie(claims, res, secret), setClaims, authTrx: ( cb: (tx: Knex.Transaction) => TResult, diff --git a/packages/api/src/routers/auth/auth_router.ts b/packages/api/src/routers/auth/auth_router.ts index 03ebddc54..32fe75122 100644 --- a/packages/api/src/routers/auth/auth_router.ts +++ b/packages/api/src/routers/auth/auth_router.ts @@ -41,16 +41,17 @@ import { comparePassword, getClaimsByToken, hashPassword, + setAuthInCookie, } from '../../utils/auth' -import { - createUser, - sendConfirmationEmail, - sendPasswordResetEmail, -} from '../../services/create_user' +import { createUser } from '../../services/create_user' import { isErrorWithCode } from '../../resolvers' import { initModels } from '../../server' import { getRepository } from '../../entity/utils' import { User } from '../../entity/user' +import { + sendConfirmationEmail, + sendPasswordResetEmail, +} from '../../services/send_emails' const logger = buildLogger('app.dispatch') const signToken = promisify(jwt.sign) @@ -325,6 +326,13 @@ export function authRouter() { } } + const message = res.get('Message') + if (message) { + return res.redirect( + `${env.client.url}/home?message=${encodeURIComponent(message)}` + ) + } + if (newUser) { if (redirectUri && redirectUri !== '/') { return res.redirect( @@ -411,13 +419,7 @@ export function authRouter() { } // set auth cookie in response header - const token = await signToken({ uid: user.id }, env.server.jwtSecret) - - res.cookie('auth', token, { - httpOnly: true, - expires: new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000), - }) - + await setAuthInCookie({ uid: user.id }, res) await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('email-login exception:', e) @@ -507,6 +509,8 @@ export function authRouter() { ) } + res.set('Message', 'CONFIRMATION_SUCCESS') + await setAuthInCookie({ uid: user.id }, res) await handleSuccessfulLogin(req, res, user, false) } catch (e) { logger.info('confirm-email exception:', e) @@ -522,18 +526,18 @@ export function authRouter() { ) router.options( - '/email-reset-password', + '/forgot-password', cors({ ...corsConfig, maxAge: 600 }) ) router.post( - '/email-reset-password', + '/forgot-password', cors(corsConfig), async (req: express.Request, res: express.Response) => { const email = req.body.email if (!email) { return res.redirect( - `${env.client.url}/email-reset-password?errorCodes=INVALID_EMAIL` + `${env.client.url}/forgot-password?errorCodes=INVALID_EMAIL` ) } @@ -543,7 +547,7 @@ export function authRouter() { }) if (!user) { return res.redirect( - `${env.client.url}/email-reset-password?errorCodes=USER_NOT_FOUND` + `${env.client.url}/forgot-password?errorCodes=USER_NOT_FOUND` ) } @@ -555,16 +559,76 @@ export function authRouter() { if (!(await sendPasswordResetEmail(user))) { return res.redirect( - `${env.client.url}/email-reset-password?errorCodes=INVALID_EMAIL` + `${env.client.url}/forgot-password?errorCodes=INVALID_EMAIL` ) } - res.redirect(`${env.client.url}/email-reset-password?message=SUCCESS`) + res.redirect(`${env.client.url}/forgot-password?message=SUCCESS`) } catch (e) { - logger.info('email-reset-password exception:', e) + logger.info('forgot-password exception:', e) + + res.redirect(`${env.client.url}/forgot-password?errorCodes=UNKNOWN`) + } + } + ) + + router.options( + '/reset-password', + cors({ ...corsConfig, maxAge: 600 }) + ) + + router.post( + '/reset-password', + cors(corsConfig), + async (req: express.Request, res: express.Response) => { + const { token, password } = req.body + if (!token || !password) { + return res.redirect( + `${env.client.url}/reset-password?errorCodes=INVALID_CREDENTIALS` + ) + } + + try { + // verify token + const claims = await getClaimsByToken(token) + if (!claims) { + return res.redirect( + `${env.client.url}/reset-password?errorCodes=INVALID_TOKEN` + ) + } + + const user = await getRepository(User).findOneBy({ id: claims.uid }) + if (!user) { + return res.redirect( + `${env.client.url}/reset-password?errorCodes=USER_NOT_FOUND` + ) + } + + if (user.status === StatusType.Pending) { + return res.redirect( + `${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION` + ) + } + + const hashedPassword = await hashPassword(password) + await getRepository(User).update( + { id: user.id }, + { password: hashedPassword } + ) + + res.set('Message', 'PASSWORD_RESET_SUCCESS') + await setAuthInCookie({ uid: user.id }, res) + await handleSuccessfulLogin(req, res, user, false) + } catch (e) { + logger.info('reset-password exception:', e) + if (e instanceof jwt.TokenExpiredError) { + return res.redirect( + `${env.client.url}/reset-password?errorCodes=TOKEN_EXPIRED` + ) + } res.redirect( - `${env.client.url}/email-reset-password?errorCodes=UNKNOWN` + `${env.client.url}/reset-password?errorCodes=INVALID_TOKEN` ) } } diff --git a/packages/api/src/services/create_user.ts b/packages/api/src/services/create_user.ts index 39cea746a..2d6267b6c 100644 --- a/packages/api/src/services/create_user.ts +++ b/packages/api/src/services/create_user.ts @@ -9,9 +9,7 @@ import { Invite } from '../entity/groups/invite' import { GroupMembership } from '../entity/groups/group_membership' import { AppDataSource } from '../server' import { getRepository } from '../entity/utils' -import { generateVerificationToken } from '../utils/auth' -import { env } from '../env' -import { sendEmail } from '../utils/sendEmail' +import { sendConfirmationEmail } from './send_emails' export const createUser = async (input: { provider: AuthProvider @@ -126,37 +124,3 @@ const getUser = async (email: string): Promise => { relations: ['profile'], }) } - -export const sendConfirmationEmail = async (user: { - id: string - name: string - email: string -}): Promise => { - // generate confirmation link - const confirmationToken = generateVerificationToken(user.id) - const confirmationLink = `${env.client.url}/confirm-email/${confirmationToken}` - // send email - return sendEmail({ - from: env.sender.message, - to: user.email, - subject: 'Confirm your email', - text: `Hey ${user.name},\n\nPlease confirm your email by clicking the link below:\n\n${confirmationLink}\n\n`, - }) -} - -export const sendPasswordResetEmail = async (user: { - id: string - name: string - email: string -}): Promise => { - // generate link - const token = generateVerificationToken(user.id) - const link = `${env.client.url}/reset-password/${token}` - // send email - return sendEmail({ - from: env.sender.message, - to: user.email, - subject: 'Reset your password', - text: `Hey ${user.name},\n\nPlease reset your password by clicking the link below:\n\n${link}\n\n`, - }) -} diff --git a/packages/api/src/services/send_emails.ts b/packages/api/src/services/send_emails.ts new file mode 100644 index 000000000..e578d4cd7 --- /dev/null +++ b/packages/api/src/services/send_emails.ts @@ -0,0 +1,37 @@ +import { generateVerificationToken } from '../utils/auth' +import { env } from '../env' +import { sendEmail } from '../utils/sendEmail' + +export const sendConfirmationEmail = async (user: { + id: string + name: string + email: string +}): Promise => { + // generate confirmation link + const confirmationToken = generateVerificationToken(user.id) + const confirmationLink = `${env.client.url}/confirm-email/${confirmationToken}` + // send email + return sendEmail({ + from: env.sender.message, + to: user.email, + subject: 'Confirm your email', + text: `Hey ${user.name},\n\nPlease confirm your email by clicking the link below:\n\n${confirmationLink}\n\n`, + }) +} + +export const sendPasswordResetEmail = async (user: { + id: string + name: string + email: string +}): Promise => { + // generate link + const token = generateVerificationToken(user.id) + const link = `${env.client.url}/reset-password/${token}` + // send email + return sendEmail({ + from: env.sender.message, + to: user.email, + subject: 'Reset your password', + text: `Hey ${user.name},\n\nPlease reset your password by clicking the link below:\n\n${link}\n\n`, + }) +} diff --git a/packages/api/src/utils/auth.ts b/packages/api/src/utils/auth.ts index 5dfb0e9b1..8885d279e 100644 --- a/packages/api/src/utils/auth.ts +++ b/packages/api/src/utils/auth.ts @@ -1,11 +1,15 @@ import * as bcrypt from 'bcryptjs' import { v4 as uuidv4 } from 'uuid' -import { Claims } from '../resolvers/types' +import { Claims, ClaimsToSet } from '../resolvers/types' import { getRepository } from '../entity/utils' import { ApiKey } from '../entity/api_key' import crypto from 'crypto' import * as jwt from 'jsonwebtoken' import { env } from '../env' +import express from 'express' +import { promisify } from 'util' + +const signToken = promisify(jwt.sign) export const hashPassword = async (password: string, salt = 10) => { return bcrypt.hash(password, salt) @@ -94,3 +98,17 @@ export const generateVerificationToken = ( return jwt.sign({ uid: userId, iat, exp }, env.server.jwtSecret) } + +export const setAuthInCookie = async ( + claims: ClaimsToSet, + res: express.Response, + secret: string = env.server.jwtSecret +) => { + // set auth cookie in response header + const token = await signToken(claims, secret) + + res.cookie('auth', token, { + httpOnly: true, + expires: new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000), + }) +} diff --git a/packages/api/test/routers/auth.test.ts b/packages/api/test/routers/auth.test.ts index e3457271c..54fd2a59f 100644 --- a/packages/api/test/routers/auth.test.ts +++ b/packages/api/test/routers/auth.test.ts @@ -335,9 +335,9 @@ describe('auth router', () => { }) }) - describe('email-reset-password', () => { + describe('forgot-password', () => { const emailResetPasswordReq = (email: string): supertest.Test => { - return request.post(`${route}/email-reset-password`).send({ + return request.post(`${route}/forgot-password`).send({ email, }) } @@ -379,10 +379,10 @@ describe('auth router', () => { sinon.restore() }) - it('redirects to email-reset-password page with success message', async () => { + it('redirects to forgot-password page with success message', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/email-reset-password?message=SUCCESS' + '/forgot-password?message=SUCCESS' ) }) }) @@ -403,7 +403,7 @@ describe('auth router', () => { it('redirects to sign up page with error code INVALID_EMAIL', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/email-reset-password?errorCodes=INVALID_EMAIL' + '/forgot-password?errorCodes=INVALID_EMAIL' ) }) }) @@ -430,10 +430,10 @@ describe('auth router', () => { email = 'non_exists_email@domain.app' }) - it('redirects to email-reset-password page with error code USER_NOT_FOUND', async () => { + it('redirects to forgot-password page with error code USER_NOT_FOUND', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/email-reset-password?errorCodes=USER_NOT_FOUND' + '/forgot-password?errorCodes=USER_NOT_FOUND' ) }) }) @@ -444,10 +444,10 @@ describe('auth router', () => { email = '' }) - it('redirects to email-reset-password page with error code INVALID_EMAIL', async () => { + it('redirects to forgot-password page with error code INVALID_EMAIL', async () => { const res = await emailResetPasswordReq(email).expect(302) expect(res.header.location).to.endWith( - '/email-reset-password?errorCodes=INVALID_EMAIL' + '/forgot-password?errorCodes=INVALID_EMAIL' ) }) })