chore: initial commit
This commit is contained in:
37
src/services/users/resetPassword/__docs__/post.yaml
Normal file
37
src/services/users/resetPassword/__docs__/post.yaml
Normal file
@ -0,0 +1,37 @@
|
||||
/users/resetPassword:
|
||||
post:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Request a password-reset change'
|
||||
description: 'Allows a user to reset his password, if he forgets thanks to his email address.'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
email:
|
||||
type: 'string'
|
||||
format: 'email'
|
||||
required:
|
||||
- 'email'
|
||||
parameters:
|
||||
- name: 'redirectURI'
|
||||
description: 'The redirect URI to redirect the user when he clicks on the button of the email, so he can change his password with a form on the frontend.'
|
||||
in: 'query'
|
||||
required: true
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
message:
|
||||
type: 'string'
|
||||
enum:
|
||||
[
|
||||
'Password-reset request successful, please check your emails!'
|
||||
]
|
33
src/services/users/resetPassword/__docs__/put.yaml
Normal file
33
src/services/users/resetPassword/__docs__/put.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
/users/resetPassword:
|
||||
put:
|
||||
tags:
|
||||
- 'users'
|
||||
summary: 'Change the user password'
|
||||
description: 'Change the user password if the provided tempToken (sent in the email of POST /users/resetPassword) is correct.'
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
password:
|
||||
type: 'string'
|
||||
format: 'password'
|
||||
example: 'password'
|
||||
tempToken:
|
||||
type: 'string'
|
||||
required:
|
||||
- 'password'
|
||||
- 'tempToken'
|
||||
responses:
|
||||
allOf:
|
||||
- $ref: '#/definitions/BadRequestError'
|
||||
- '200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: 'object'
|
||||
properties:
|
||||
message:
|
||||
type: 'string'
|
||||
enum: ['The new password has been saved!']
|
97
src/services/users/resetPassword/__test__/post.test.ts
Normal file
97
src/services/users/resetPassword/__test__/post.test.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import ms from 'ms'
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import { errorsMessages as errorsConfirmed } from '../../../../tools/middlewares/authenticateUser'
|
||||
import User from '../../../../models/User'
|
||||
import { errorsMessages } from '..'
|
||||
|
||||
describe('POST /users/resetPassword', () => {
|
||||
it('succeeds with valid email and generate a tempToken', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: true })
|
||||
const userBefore = await User.findOne({ where: { name } })
|
||||
expect(userBefore).not.toBeNull()
|
||||
expect(userBefore?.tempToken).toBe(null)
|
||||
expect(userBefore?.tempExpirationToken).toBe(null)
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const userAfter = await User.findOne({ where: { name } })
|
||||
expect(userAfter?.tempToken).not.toBeNull()
|
||||
expect(userAfter?.tempExpirationToken).not.toBeNull()
|
||||
})
|
||||
|
||||
it('succeeds even if there is already a password-reset request in progress (but outdated)', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: true })
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
if (user != null) {
|
||||
user.tempExpirationToken = Date.now() - ms('2 hour')
|
||||
await user.save()
|
||||
}
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it("fails with email address that doesn't exist", async () => {
|
||||
const response = await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email: 'contact@test.com' })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(errorsMessages.email.notExist)
|
||||
})
|
||||
|
||||
it('fails with unconfirmed account', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: false })
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(errorsConfirmed.invalidAccount)
|
||||
})
|
||||
|
||||
it('fails if there is already a password-reset request in progress', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
await authenticateUserTest({ email, name, shouldBeConfirmed: true })
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const response = await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.password.alreadyInProgress
|
||||
)
|
||||
})
|
||||
})
|
95
src/services/users/resetPassword/__test__/put.test.ts
Normal file
95
src/services/users/resetPassword/__test__/put.test.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import ms from 'ms'
|
||||
import request from 'supertest'
|
||||
|
||||
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
|
||||
import application from '../../../../application'
|
||||
import User from '../../../../models/User'
|
||||
import { errorsMessages } from '..'
|
||||
|
||||
describe('PUT /users/resetPassword', () => {
|
||||
it('succeeds and change the password so we can signin again', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
await authenticateUserTest({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
shouldBeConfirmed: true
|
||||
})
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
|
||||
const newPassword = 'newpassword'
|
||||
await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send({ password: newPassword, tempToken: user?.tempToken })
|
||||
.expect(200)
|
||||
|
||||
await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password: newPassword })
|
||||
.expect(200)
|
||||
})
|
||||
|
||||
it('fails with an invalid "tempToken"', async () => {
|
||||
const response = await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send({ password: 'newpassword', tempToken: 'sometemptoken' })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.tempToken.invalid
|
||||
)
|
||||
})
|
||||
|
||||
it('fails if there is no password and tempToken provided', async () => {
|
||||
const response = await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send()
|
||||
.expect(400)
|
||||
expect(response.body.errors.length).toEqual(2)
|
||||
})
|
||||
|
||||
it('fails if the tempToken is outdated', async () => {
|
||||
const email = 'contact@test.com'
|
||||
const name = 'John'
|
||||
const password = 'test'
|
||||
await authenticateUserTest({
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
shouldBeConfirmed: true
|
||||
})
|
||||
|
||||
await request(application)
|
||||
.post('/users/resetPassword?redirectURI=someurl.com')
|
||||
.send({ email })
|
||||
.expect(200)
|
||||
|
||||
const user = await User.findOne({ where: { name } })
|
||||
expect(user).not.toBeNull()
|
||||
if (user != null) {
|
||||
user.tempExpirationToken = Date.now() - ms('2 hour')
|
||||
await user.save()
|
||||
}
|
||||
|
||||
const newPassword = 'newpassword'
|
||||
const response = await request(application)
|
||||
.put('/users/resetPassword')
|
||||
.send({ password: newPassword, tempToken: user?.tempToken })
|
||||
.expect(400)
|
||||
|
||||
expect(response.body.errors.length).toEqual(1)
|
||||
expect(response.body.errors[0].message).toBe(
|
||||
errorsMessages.tempToken.invalid
|
||||
)
|
||||
})
|
||||
})
|
22
src/services/users/resetPassword/index.ts
Normal file
22
src/services/users/resetPassword/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { postResetPasswordRouter } from './post'
|
||||
import { putResetPasswordRouter } from './put'
|
||||
|
||||
export const resetPasswordRouter = Router()
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
notExist: "Email address doesn't exist"
|
||||
},
|
||||
password: {
|
||||
alreadyInProgress: 'A request to reset-password is already in progress'
|
||||
},
|
||||
tempToken: {
|
||||
invalid: '"tempToken" is invalid'
|
||||
}
|
||||
}
|
||||
|
||||
resetPasswordRouter.use('/', putResetPasswordRouter)
|
||||
resetPasswordRouter.use('/', postResetPasswordRouter)
|
73
src/services/users/resetPassword/post.ts
Normal file
73
src/services/users/resetPassword/post.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import ms from 'ms'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { errorsMessages as errorsConfirmed } from '../../../tools/middlewares/authenticateUser'
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import UserSetting from '../../../models/UserSetting'
|
||||
import { sendEmail } from '../../../tools/email/sendEmail'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
notExist: "Email address doesn't exist"
|
||||
},
|
||||
password: {
|
||||
alreadyInProgress: 'A request to reset-password is already in progress'
|
||||
},
|
||||
tempToken: {
|
||||
invalid: '"tempToken" is invalid'
|
||||
}
|
||||
}
|
||||
|
||||
export const postResetPasswordRouter = Router()
|
||||
|
||||
postResetPasswordRouter.post(
|
||||
'/users/resetPassword',
|
||||
[
|
||||
body('email')
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage(errorsMessages.email.mustBeValid),
|
||||
query('redirectURI').notEmpty().trim()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { email } = req.body as { email: string }
|
||||
const { redirectURI } = req.query as { redirectURI: string }
|
||||
|
||||
const user = await User.findOne({ where: { email } })
|
||||
if (user == null) {
|
||||
throw new BadRequestError(errorsMessages.email.notExist)
|
||||
}
|
||||
if (!user.isConfirmed) {
|
||||
throw new BadRequestError(errorsConfirmed.invalidAccount)
|
||||
}
|
||||
const isValidTempToken =
|
||||
user.tempExpirationToken != null && user.tempExpirationToken > Date.now()
|
||||
if (user.tempToken != null && isValidTempToken) {
|
||||
throw new BadRequestError(errorsMessages.password.alreadyInProgress)
|
||||
}
|
||||
|
||||
const tempToken = uuidv4()
|
||||
user.tempToken = tempToken
|
||||
user.tempExpirationToken = Date.now() + ms('1 hour')
|
||||
await user.save()
|
||||
const userSettings = await UserSetting.findOne({
|
||||
where: { userId: user.id }
|
||||
})
|
||||
await sendEmail({
|
||||
type: 'reset-password',
|
||||
email,
|
||||
url: `${redirectURI}?tempToken=${tempToken}`,
|
||||
language: userSettings?.language,
|
||||
theme: userSettings?.theme
|
||||
})
|
||||
return res.status(200).json({
|
||||
message: 'Password-reset request successful, please check your emails!'
|
||||
})
|
||||
}
|
||||
)
|
53
src/services/users/resetPassword/put.ts
Normal file
53
src/services/users/resetPassword/put.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { body } from 'express-validator'
|
||||
|
||||
import { validateRequest } from '../../../tools/middlewares/validateRequest'
|
||||
import User from '../../../models/User'
|
||||
import { BadRequestError } from '../../../tools/errors/BadRequestError'
|
||||
|
||||
export const errorsMessages = {
|
||||
email: {
|
||||
mustBeValid: 'Email must be valid',
|
||||
notExist: "Email address doesn't exist"
|
||||
},
|
||||
password: {
|
||||
alreadyInProgress: 'A request to reset-password is already in progress'
|
||||
},
|
||||
tempToken: {
|
||||
invalid: '"tempToken" is invalid'
|
||||
}
|
||||
}
|
||||
|
||||
export const putResetPasswordRouter = Router()
|
||||
|
||||
putResetPasswordRouter.put(
|
||||
'/users/resetPassword',
|
||||
[
|
||||
body('password')
|
||||
.trim()
|
||||
.notEmpty(),
|
||||
body('tempToken')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
],
|
||||
validateRequest,
|
||||
async (req: Request, res: Response) => {
|
||||
const { password, tempToken } = req.body as {
|
||||
password: string
|
||||
tempToken: string
|
||||
}
|
||||
const user = await User.findOne({ where: { tempToken } })
|
||||
const isValidTempToken =
|
||||
user?.tempExpirationToken != null && user.tempExpirationToken > Date.now()
|
||||
if (user == null || !isValidTempToken) {
|
||||
throw new BadRequestError(errorsMessages.tempToken.invalid)
|
||||
}
|
||||
const hashedPassword = await bcrypt.hash(password, 12)
|
||||
user.password = hashedPassword
|
||||
user.tempToken = null
|
||||
user.tempExpirationToken = null
|
||||
await user.save()
|
||||
return res.status(200).json({ message: 'The new password has been saved!' })
|
||||
}
|
||||
)
|
Reference in New Issue
Block a user