add router handler for reset password
This commit is contained in:
committed by
Jackson Harper
parent
286d167769
commit
6699ec834d
@ -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,
|
||||
|
||||
@ -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`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`,
|
||||
})
|
||||
}
|
||||
|
||||
37
packages/api/src/services/send_emails.ts
Normal file
37
packages/api/src/services/send_emails.ts
Normal 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`,
|
||||
})
|
||||
}
|
||||
@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user