chore: initial commit

This commit is contained in:
Divlo
2021-10-24 04:06:16 +02:00
commit 714cc643ba
260 changed files with 40783 additions and 0 deletions

View 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!'
]

View 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!']

View 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
)
})
})

View 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
)
})
})

View 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)

View 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!'
})
}
)

View 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!' })
}
)