add router handler for reset password

This commit is contained in:
Hongbo Wu
2022-07-22 16:38:21 +08:00
committed by Jackson Harper
parent 286d167769
commit 6699ec834d
6 changed files with 152 additions and 76 deletions

View File

@ -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<ExpressContext, ResolverContext> = 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: <TResult>(
cb: (tx: Knex.Transaction) => TResult,

View File

@ -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<express.Request>({ ...corsConfig, maxAge: 600 })
)
router.post(
'/email-reset-password',
'/forgot-password',
cors<express.Request>(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<express.Request>({ ...corsConfig, maxAge: 600 })
)
router.post(
'/reset-password',
cors<express.Request>(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`
)
}
}

View File

@ -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<User | null> => {
relations: ['profile'],
})
}
export const sendConfirmationEmail = async (user: {
id: string
name: string
email: string
}): Promise<boolean> => {
// 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<boolean> => {
// 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`,
})
}

View File

@ -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<boolean> => {
// 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<boolean> => {
// 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`,
})
}

View File

@ -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),
})
}

View File

@ -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'
)
})
})