add router handler for email reset password

This commit is contained in:
Hongbo Wu
2022-07-22 15:37:42 +08:00
committed by Jackson Harper
parent 53fd7f52df
commit 286d167769
3 changed files with 218 additions and 20 deletions

View File

@ -42,7 +42,11 @@ import {
getClaimsByToken,
hashPassword,
} from '../../utils/auth'
import { createUser, sendConfirmationEmail } from '../../services/create_user'
import {
createUser,
sendConfirmationEmail,
sendPasswordResetEmail,
} from '../../services/create_user'
import { isErrorWithCode } from '../../resolvers'
import { initModels } from '../../server'
import { getRepository } from '../../entity/utils'
@ -302,7 +306,7 @@ export function authRouter() {
async function handleSuccessfulLogin(
req: express.Request,
res: express.Response,
user: UserData,
user: UserData | User,
newUser: boolean
): Promise<void> {
try {
@ -363,6 +367,12 @@ export function authRouter() {
async (req: express.Request, res: express.Response) => {
const { email, password } = req.body
if (!email || !password) {
return res.redirect(
`${env.client.url}/email-login?errorCodes=${LoginErrorCode.InvalidCredentials}`
)
}
try {
const models = initModels(kx, false)
const user = await models.user.getWhere({
@ -426,6 +436,12 @@ export function authRouter() {
cors<express.Request>(corsConfig),
async (req: express.Request, res: express.Response) => {
const { email, password, name, username, bio, pictureUrl } = req.body
if (!email || !password || !name || !username) {
return res.redirect(
`${env.client.url}/email-signup?errorCodes=INVALID_CREDENTIALS`
)
}
const lowerCasedUsername = username.toLowerCase()
try {
@ -491,7 +507,7 @@ export function authRouter() {
)
}
res.redirect(`${env.client.url}/email-login?message=EMAIL_VERIFIED`)
await handleSuccessfulLogin(req, res, user, false)
} catch (e) {
logger.info('confirm-email exception:', e)
if (e instanceof jwt.TokenExpiredError) {
@ -505,5 +521,54 @@ export function authRouter() {
}
)
router.options(
'/email-reset-password',
cors<express.Request>({ ...corsConfig, maxAge: 600 })
)
router.post(
'/email-reset-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`
)
}
try {
const user = await getRepository(User).findOneBy({
email,
})
if (!user) {
return res.redirect(
`${env.client.url}/email-reset-password?errorCodes=USER_NOT_FOUND`
)
}
if (user.status === StatusType.Pending) {
return res.redirect(
`${env.client.url}/email-login?errorCodes=PENDING_VERIFICATION`
)
}
if (!(await sendPasswordResetEmail(user))) {
return res.redirect(
`${env.client.url}/email-reset-password?errorCodes=INVALID_EMAIL`
)
}
res.redirect(`${env.client.url}/email-reset-password?message=SUCCESS`)
} catch (e) {
logger.info('email-reset-password exception:', e)
res.redirect(
`${env.client.url}/email-reset-password?errorCodes=UNKNOWN`
)
}
}
)
return router
}

View File

@ -143,3 +143,20 @@ export const sendConfirmationEmail = async (user: {
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

@ -12,21 +12,21 @@ import { generateVerificationToken, hashPassword } from '../../src/utils/auth'
describe('auth router', () => {
const route = '/api/auth'
const signupRequest = (
email: string,
password: string,
name: string,
username: string
): supertest.Test => {
return request.post(`${route}/email-signup`).send({
email,
password,
name,
username,
})
}
describe('email signup', () => {
const signupRequest = (
email: string,
password: string,
name: string,
username: string
): supertest.Test => {
return request.post(`${route}/email-signup`).send({
email,
password,
name,
username,
})
}
const validPassword = 'validPassword'
let email: string
@ -284,11 +284,9 @@ describe('auth router', () => {
token = generateVerificationToken(user.id)
})
it('redirects to email-login page', async () => {
it('logs in and redirects to home page', async () => {
const res = await confirmEmailRequest(token).expect(302)
expect(res.header.location).to.endWith(
'/email-login?message=EMAIL_VERIFIED'
)
expect(res.header.location).to.endWith('/home')
})
it('sets user as active', async () => {
@ -336,4 +334,122 @@ describe('auth router', () => {
})
})
})
describe('email-reset-password', () => {
const emailResetPasswordReq = (email: string): supertest.Test => {
return request.post(`${route}/email-reset-password`).send({
email,
})
}
let email: string
context('when email is not empty', () => {
before(() => {
email = `some_email@domain.app`
})
context('when user exists', () => {
let user: User
before(async () => {
user = await createTestUser('test_user')
email = user.email
})
after(async () => {
await deleteTestUser(user.name)
})
context('when email is verified', () => {
let fake: (msg: MailDataRequired) => Promise<boolean>
before(async () => {
await getRepository(User).update(user.id, {
status: StatusType.Active,
})
})
context('when reset password email sent', () => {
before(() => {
fake = sinon.replace(util, 'sendEmail', sinon.fake.resolves(true))
})
after(() => {
sinon.restore()
})
it('redirects to email-reset-password page with success message', async () => {
const res = await emailResetPasswordReq(email).expect(302)
expect(res.header.location).to.endWith(
'/email-reset-password?message=SUCCESS'
)
})
})
context('when reset password email not sent', () => {
before(() => {
fake = sinon.replace(
util,
'sendEmail',
sinon.fake.resolves(false)
)
})
after(() => {
sinon.restore()
})
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'
)
})
})
})
context('when email is not verified', () => {
before(async () => {
await getRepository(User).update(user.id, {
status: StatusType.Pending,
})
})
it('redirects to email-login page with error code PENDING_VERIFICATION', async () => {
const res = await emailResetPasswordReq(email).expect(302)
expect(res.header.location).to.endWith(
'/email-login?errorCodes=PENDING_VERIFICATION'
)
})
})
})
context('when user does not exist', () => {
before(() => {
email = 'non_exists_email@domain.app'
})
it('redirects to email-reset-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'
)
})
})
})
context('when email is empty', () => {
before(() => {
email = ''
})
it('redirects to email-reset-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'
)
})
})
})
})