feat: migrate from express to fastify

This commit is contained in:
Divlo
2021-10-24 04:18:18 +02:00
parent 714cc643ba
commit b77e602358
281 changed files with 19768 additions and 22895 deletions

View File

@ -1,21 +0,0 @@
/users/current:
get:
security:
- bearerAuth: []
tags:
- 'users'
summary: 'GET the current connected user'
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/User'
- $ref: '#/definitions/UserSettingsObject'
- $ref: '#/definitions/UserCurrentStrategy'
- $ref: '#/definitions/UserStrategies'

View File

@ -1,45 +0,0 @@
/users/current:
put:
security:
- bearerAuth: []
tags:
- 'users'
summary: 'Edit the current connected user info'
requestBody:
content:
multipart/form-data:
schema:
type: 'object'
properties:
email:
type: 'string'
format: 'email'
name:
type: 'string'
minLength: 3
maxLength: 30
example: 'user'
biography:
type: 'string'
maxLength: 160
example: 'biography'
status:
type: 'string'
maxLength: 100
example: '👀 Working on secrets projects...'
logo:
type: 'string'
format: 'binary'
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/PayloadTooLargeError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/User'
- $ref: '#/definitions/UserCurrentStrategy'

View File

@ -1,46 +1,29 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import application from '../../../../application'
import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
describe('GET /users/current', () => {
it('succeeds with valid Bearer accessToken', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.get('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.settings).not.toBeNull()
expect(response.body.currentStrategy).toEqual('local')
expect(Array.isArray(response.body.strategies)).toBeTruthy()
expect(response.body.strategies.includes('local')).toBeTruthy()
it('succeeds', async () => {
const { accessToken, user } = await authenticateUserTest()
const response = await application.inject({
method: 'GET',
url: '/users/current',
headers: {
authorization: `Bearer ${accessToken}`
}
})
const responseJson = response.json()
expect(response.statusCode).toEqual(200)
expect(responseJson.user.name).toEqual(user.name)
expect(responseJson.user.strategies).toEqual(
expect.arrayContaining(['local'])
)
})
it('fails with unconfirmed account', async () => {
const userToken = await authenticateUserTest({ shouldBeConfirmed: false })
const response = await request(application)
.get('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(401)
expect(response.body.errors.length).toEqual(1)
})
it('fails ForbiddenError with invalid Bearer accessToken', async () => {
await request(application)
.get('/users/current')
.set('Authorization', 'Bearer invalidtoken')
.send()
.expect(403)
})
it('fails NotAuthorizedError with invalid accessToken', async () => {
await request(application)
.get('/users/current')
.set('Authorization', 'invalidtoken')
.send()
.expect(401)
it('fails with unauthenticated user', async () => {
const response = await application.inject({
method: 'GET',
url: '/users/current'
})
expect(response.statusCode).toEqual(401)
})
})

View File

@ -1,228 +1,107 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import User from '../../../../models/User'
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
import { randomString } from '../../../../tools/utils/random'
import { errorsMessages } from '../index'
import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../../__test__/setup.js'
import { userExample } from '../../../../models/User.js'
describe('PUT /users/current', () => {
it('succeeds with valid accessToken, valid email and valid name', async () => {
const name = 'test2'
const email = 'test2@test2.com'
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name, email })
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.user.name).toBe(name)
expect(response.body.user.email).toBe(email)
expect(response.body.currentStrategy).not.toBeNull()
})
it('succeeds and only change the email', async () => {
const name = 'John'
const email = 'contact@test.com'
const userToken = await authenticateUserTest({
name,
email
it('succeeds with valid accessToken and valid name', async () => {
const newName = 'John Doe'
const { accessToken, user } = await authenticateUserTest()
prismaMock.user.update.mockResolvedValue({
...user,
name: newName
})
let user = (await User.findAll())[0]
expect(user.email).toEqual(email)
expect(user.name).toEqual(name)
const email2 = 'test2@test2.com'
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ email: email2 })
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.user.email).toEqual(email2)
expect(response.body.user.name).toEqual(name)
user = (await User.findAll())[0]
expect(user.email).toEqual(email2)
expect(user.name).toEqual(name)
expect(user.isConfirmed).toBe(false)
})
it('succeeds and only change the name', async () => {
const name = 'John'
const email = 'contact@test.com'
const userToken = await authenticateUserTest({
name,
email
const response = await application.inject({
method: 'PUT',
url: '/users/current',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: {
name: newName
}
})
let user = (await User.findAll())[0]
expect(user.email).toEqual(email)
expect(user.name).toEqual(name)
const name2 = 'test2'
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name: name2 })
.expect(200)
expect(response.body.user).not.toBeNull()
user = (await User.findAll())[0]
expect(user.email).toEqual(email)
expect(user.name).toEqual(name2)
const responseJson = response.json()
expect(response.statusCode).toEqual(200)
expect(responseJson.user.name).toEqual(newName)
})
it('succeeds and only change the status', async () => {
const userToken = await authenticateUserTest()
const status = '👀 Working on secret projects...'
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ status })
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.user.status).toBe(status)
})
it('succeeds and only change the biography', async () => {
const userToken = await authenticateUserTest()
const biography = 'My awesome biography'
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ biography })
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.user.biography).toBe(biography)
})
it('fails with unconfirmed account', async () => {
const userToken = await authenticateUserTest({
name: 'John',
email: 'contact@john.com',
shouldBeConfirmed: false
it('succeeds and only update the status', async () => {
const newStatus = '👀 Working on secret projects...'
const { accessToken, user } = await authenticateUserTest()
prismaMock.user.update.mockResolvedValue({
...user,
status: newStatus
})
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(401)
expect(response.body.errors.length).toEqual(1)
})
it('fails with invalid status', async () => {
const userToken = await authenticateUserTest()
const status = randomString(110)
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ status })
.expect(400)
expect(response.body.errors.length).toEqual(1)
expect(response.body.errors[0].message).toEqual(
commonErrorsMessages.charactersLength('status', { max: 100 })
)
})
it('fails with invalid biography', async () => {
const userToken = await authenticateUserTest()
const biography = randomString(170)
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ biography })
.expect(400)
expect(response.body.errors.length).toEqual(1)
expect(response.body.errors[0].message).toEqual(
commonErrorsMessages.charactersLength('biography', { max: 160 })
)
})
it('fails with invalid name and invalid email', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({
name: 'jo',
email: 'test2@test2'
})
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(2)
expect(errors).toEqual(
expect.arrayContaining([
errorsMessages.email.mustBeValid,
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
])
)
})
it('fails with name and email already used', async () => {
const firstUserName = 'Test'
const firstUserEmail = 'test@test.com'
await authenticateUserTest({
name: firstUserName,
email: firstUserEmail,
shouldBeConfirmed: true
const response = await application.inject({
method: 'PUT',
url: '/users/current',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: {
status: newStatus
}
})
const secondUserToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current')
.set(
'Authorization',
`${secondUserToken.type} ${secondUserToken.accessToken}`
)
.send({
name: firstUserName,
email: firstUserEmail
})
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(2)
expect(errors).toEqual(
expect.arrayContaining(['Name already used', 'Email already used'])
)
const responseJson = response.json()
expect(response.statusCode).toEqual(200)
expect(responseJson.user.name).toEqual(user.name)
expect(responseJson.user.status).toEqual(newStatus)
})
it('fails with name identical to the current user name', async () => {
const name = 'Test'
const email = 'test@test.com'
const userToken = await authenticateUserTest({
name,
email
it('fails with name already used', async () => {
const newName = 'John Doe'
prismaMock.user.findFirst.mockResolvedValue(userExample)
const { accessToken } = await authenticateUserTest()
const response = await application.inject({
method: 'PUT',
url: '/users/current',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: {
name: newName
}
})
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors).toEqual(
expect.arrayContaining([errorsMessages.name.alreadyConnected])
)
expect(response.statusCode).toEqual(400)
})
it('fails with email identical to the current user email', async () => {
const name = 'Test'
const email = 'test@test.com'
const userToken = await authenticateUserTest({
name,
email
it('fails with invalid website url', async () => {
const newWebsite = 'invalid website url'
const { accessToken } = await authenticateUserTest()
const response = await application.inject({
method: 'PUT',
url: '/users/current',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: {
website: newWebsite
}
})
const response = await request(application)
.put('/users/current')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ email })
.expect(400)
expect(response.statusCode).toEqual(400)
})
const errors = formatErrors(response.body.errors)
expect(errors).toEqual(
expect.arrayContaining([errorsMessages.email.alreadyConnected])
)
it('suceeds with valid website url', async () => {
const newWebsite = 'https://somerandomwebsite.com'
const { accessToken, user } = await authenticateUserTest()
prismaMock.user.update.mockResolvedValue({
...user,
website: newWebsite
})
const response = await application.inject({
method: 'PUT',
url: '/users/current',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: {
website: newWebsite
}
})
const responseJson = response.json()
expect(response.statusCode).toEqual(200)
expect(responseJson.user.name).toEqual(user.name)
expect(responseJson.user.website).toEqual(newWebsite)
})
})

View File

@ -1,36 +1,59 @@
import { Request, Response, Router } from 'express'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import OAuth, { AuthenticationStrategy } from '../../../models/OAuth'
import UserSetting from '../../../models/UserSetting'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { userCurrentSchema } from '../../../models/User.js'
export const getCurrentRouter = Router()
getCurrentRouter.get(
'/users/current',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
const getCurrentUserSchema: FastifySchema = {
description: 'GET the current connected user',
tags: ['users'] as string[],
security: [
{
bearerAuth: []
}
const settings = await UserSetting.findOne({
where: { userId: req.user.current.id }
})
const OAuths = await OAuth.findAll({
where: { userId: req.user.current.id }
})
const strategies: AuthenticationStrategy[] = OAuths.map((oauth) => {
return oauth.provider
})
if (req.user.current.password != null) {
strategies.push('local')
}
return res.status(200).json({
user: req.user.current,
settings,
currentStrategy: req.user.currentStrategy,
strategies
})
] as Array<{ [key: string]: [] }>,
response: {
200: userCurrentSchema,
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
500: fastifyErrors[500]
}
)
} as const
export const getCurrentUser: FastifyPluginAsync = async (fastify) => {
await fastify.register(authenticateUser)
fastify.route({
method: 'GET',
url: '/users/current',
schema: getCurrentUserSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const settings = await prisma.userSetting.findFirst({
where: { userId: request.user.current.id }
})
const OAuths = await prisma.oAuth.findMany({
where: { userId: request.user.current.id }
})
const strategies = OAuths.map((oauth) => {
return oauth.provider
})
if (request.user.current.password != null) {
strategies.push('local')
}
reply.statusCode = 200
return {
user: {
...request.user.current,
settings,
currentStrategy: request.user.currentStrategy,
strategies
}
}
}
})
}

View File

@ -1,21 +0,0 @@
import { Router } from 'express'
import { getCurrentRouter } from './get'
import { putCurrentRouter } from './put'
import { currentSettingsRouter } from './settings'
export const currentRouter = Router()
export const errorsMessages = {
email: {
mustBeValid: 'Email must be valid',
alreadyConnected: 'You are already connected with this email address'
},
name: {
alreadyConnected: 'You are already connected with this name'
}
}
currentRouter.use('/', getCurrentRouter)
currentRouter.use('/', putCurrentRouter)
currentRouter.use('/', currentSettingsRouter)

View File

@ -0,0 +1,97 @@
import fs from 'node:fs'
import { URL } from 'node:url'
import { randomUUID } from 'node:crypto'
import { Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart, { Multipart } from 'fastify-multipart'
import {
maximumImageSize,
supportedImageMimetype,
ROOT_URL
} from '../../../../tools/configurations'
import prisma from '../../../../tools/database/prisma.js'
const putServiceSchema: FastifySchema = {
description: 'Edit the current connected user logo',
tags: ['users'] as string[],
consumes: ['multipart/form-data'] as string[],
produces: ['application/json'] as string[],
security: [
{
bearerAuth: []
}
] as Array<{ [key: string]: [] }>,
response: {
200: Type.Object({
user: Type.Object({
logo: Type.String()
})
}),
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
431: fastifyErrors[431],
500: fastifyErrors[500]
}
} as const
export const putCurrentUserLogo: FastifyPluginAsync = async (fastify) => {
await fastify.register(authenticateUser)
await fastify.register(fastifyMultipart)
fastify.route({
method: 'PUT',
url: '/users/current/logo',
schema: putServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
let files: Multipart[] = []
try {
files = await request.saveRequestFiles({
limits: {
files: 1,
fileSize: maximumImageSize * 1024 * 1024
}
})
} catch (error) {
throw fastify.httpErrors.requestHeaderFieldsTooLarge(
`body.logo should be less than ${maximumImageSize}mb.`
)
}
if (files.length !== 1) {
throw fastify.httpErrors.badRequest('You must upload at most one file.')
}
const image = files[0]
if (!supportedImageMimetype.includes(image.mimetype)) {
throw fastify.httpErrors.badRequest(
`The file must have a valid type (${supportedImageMimetype.join(
', '
)}).`
)
}
const splitedMimetype = image.mimetype.split('/')
const imageExtension = splitedMimetype[1]
const logoPath = `uploads/users/${randomUUID()}.${imageExtension}`
const logoURL = new URL(logoPath, ROOT_URL)
const logo = `/${logoPath}`
await fs.promises.copyFile(image.filepath, logoURL)
await prisma.user.update({
where: { id: request.user.current.id },
data: { logo }
})
reply.statusCode = 200
return {
user: {
logo
}
}
}
})
}

View File

@ -1,141 +1,136 @@
import { Request, Response, Router } from 'express'
import fileUpload from 'express-fileupload'
import { body, query } from 'express-validator'
import { v4 as uuidv4 } from 'uuid'
import { randomUUID } from 'node:crypto'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../tools/middlewares/validateRequest'
import User from '../../../models/User'
import {
commonErrorsMessages,
imageFileUploadOptions,
usersLogoPath
} from '../../../tools/configurations/constants'
import { alreadyUsedValidation } from '../../../tools/validations/alreadyUsedValidation'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { uploadImage } from '../../../tools/utils/uploadImage'
import { deleteEveryRefreshTokens } from '../__utils__/deleteEveryRefreshTokens'
import UserSetting from '../../../models/UserSetting'
import { sendEmail } from '../../../tools/email/sendEmail'
import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
export const errorsMessages = {
email: {
mustBeValid: 'Email must be valid',
alreadyConnected: 'You are already connected with this email address'
},
name: {
alreadyConnected: 'You are already connected with this name'
import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { userCurrentSchema, userSchema } from '../../../models/User.js'
import { sendEmail } from '../../../tools/email/sendEmail.js'
import { HOST, PORT } from '../../../tools/configurations/index.js'
import { Language, Theme } from '../../../models/UserSettings.js'
const bodyPutServiceSchema = Type.Object({
name: Type.Optional(userSchema.name),
email: Type.Optional(userSchema.email),
status: Type.Optional(userSchema.status),
biography: Type.Optional(userSchema.biography),
website: Type.Optional(userSchema.website)
})
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const queryPutCurrentUserSchema = Type.Object({
redirectURI: Type.Optional(Type.String({ format: 'uri-reference' }))
})
type QueryPutCurrentUserSchemaType = Static<typeof queryPutCurrentUserSchema>
const putServiceSchema: FastifySchema = {
description: 'Edit the current connected user information',
tags: ['users'] as string[],
security: [
{
bearerAuth: []
}
] as Array<{ [key: string]: [] }>,
body: bodyPutServiceSchema,
querystring: queryPutCurrentUserSchema,
response: {
200: userCurrentSchema,
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
500: fastifyErrors[500]
}
}
} as const
export const putCurrentRouter = Router()
export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
await fastify.register(authenticateUser)
putCurrentRouter.put(
'/users/current',
authenticateUser,
fileUpload(imageFileUploadOptions),
[
body('email')
.optional({ nullable: true })
.trim()
.isEmail()
.withMessage(errorsMessages.email.mustBeValid)
.custom(async (email: string, meta) => {
if (email === meta.req.user?.current.email) {
return await Promise.reject(
new Error(errorsMessages.email.alreadyConnected)
)
}
return await alreadyUsedValidation(User, 'email', email)
}),
body('name')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 30, min: 3 })
.withMessage(
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
)
.custom(async (name: string, meta) => {
if (name === meta.req.user?.current.name) {
return await Promise.reject(
new Error(errorsMessages.name.alreadyConnected)
)
}
return await alreadyUsedValidation(User, 'name', name)
}),
body('biography')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 160 })
.withMessage(
commonErrorsMessages.charactersLength('biography', { max: 160 })
),
body('status')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 100 })
.withMessage(
commonErrorsMessages.charactersLength('status', { max: 100 })
),
query('redirectURI').optional({ nullable: true }).trim()
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { name, email, status, biography } = req.body as {
name?: string
email?: string
status?: string
biography?: string
}
const logo = req.files?.logo
const { redirectURI } = req.query as { redirectURI?: string }
const user = req.user.current
user.name = name ?? user.name
user.status = status ?? user.status
user.biography = biography ?? user.biography
const resultUpload = await uploadImage({
image: logo,
propertyName: 'logo',
oldImage: user.logo,
imagesPath: usersLogoPath.filePath
})
if (resultUpload != null) {
user.logo = `${usersLogoPath.name}/${resultUpload}`
}
if (email != null) {
user.email = email
if (req.user.currentStrategy === 'local') {
await deleteEveryRefreshTokens(user.id)
fastify.route<{
Body: BodyPutServiceSchemaType
Params: QueryPutCurrentUserSchemaType
}>({
method: 'PUT',
url: '/users/current',
schema: putServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const tempToken = uuidv4()
user.tempToken = tempToken
user.isConfirmed = false
const userSettings = await UserSetting.findOne({
where: { userId: user.id }
const { name, email, status, biography, website } = request.body
const { redirectURI } = request.params
const userValidation = await prisma.user.findFirst({
where: {
OR: [{ email }, { name }],
AND: [{ id: { not: request.user.current.id } }]
}
})
const redirectQuery =
redirectURI != null ? `&redirectURI=${redirectURI}` : ''
await sendEmail({
type: 'confirm-email',
email,
url: `${process.env.API_BASE_URL}/users/confirmEmail?tempToken=${tempToken}${redirectQuery}`,
language: userSettings?.language,
theme: userSettings?.theme
if (userValidation != null) {
throw fastify.httpErrors.badRequest(
'body.email or body.name already taken.'
)
}
const settings = await prisma.userSetting.findFirst({
where: { userId: request.user.current.id }
})
if (settings == null) {
throw fastify.httpErrors.internalServerError()
}
const OAuths = await prisma.oAuth.findMany({
where: { userId: request.user.current.id }
})
const strategies = OAuths.map((oauth) => {
return oauth.provider
})
if (request.user.current.password != null) {
strategies.push('local')
}
if (email != null) {
await prisma.refreshToken.deleteMany({
where: {
userId: request.user.current.id
}
})
const temporaryToken = randomUUID()
const redirectQuery =
redirectURI != null ? `&redirectURI=${redirectURI}` : ''
await sendEmail({
type: 'confirm-email',
email,
url: `${request.protocol}://${HOST}:${PORT}/users/confirm-email?temporaryToken=${temporaryToken}${redirectQuery}`,
language: settings.language as Language,
theme: settings.theme as Theme
})
await prisma.user.update({
where: { id: request.user.current.id },
data: {
email,
temporaryToken,
isConfirmed: false
}
})
}
const user = await prisma.user.update({
where: { id: request.user.current.id },
data: {
name: name ?? request.user.current.name,
status: status ?? request.user.current.status,
biography: biography ?? request.user.current.biography,
website: website ?? request.user.current.website
}
})
reply.statusCode = 200
return {
user: {
...user,
settings,
currentStrategy: request.user.currentStrategy,
strategies
}
}
}
const userSaved = await user.save()
return res
.status(200)
.json({ user: userSaved, strategy: req.user.currentStrategy })
}
)
})
}

View File

@ -1,24 +0,0 @@
/users/current/settings:
put:
security:
- bearerAuth: []
tags:
- 'users'
summary: 'Edit the current connected user settings'
requestBody:
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/UserSettings'
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/UserSettingsObject'

View File

@ -1,66 +1,56 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import application from '../../../../../application'
import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../../../__test__/setup.js'
import { userSettingsExample } from '../../../../../models/UserSettings.js'
describe('PUT /users/current/settings', () => {
it('should succeeds and edit theme, language and isPublicEmail', async () => {
const isPublicEmail = true
const theme = 'light'
const language = 'fr'
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current/settings')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ isPublicEmail, theme, language })
.expect(200)
expect(response.body.settings).not.toBeNull()
expect(response.body.settings.theme).toEqual(theme)
expect(response.body.settings.language).toEqual(language)
expect(response.body.settings.isPublicEmail).toEqual(isPublicEmail)
})
it('fails with unconfirmed account', async () => {
const userToken = await authenticateUserTest({
name: 'John',
email: 'contact@john.com',
shouldBeConfirmed: false
it('succeeds and edit the theme, language, isPublicEmail and isPublicGuilds', async () => {
const newSettings = {
theme: 'light',
language: 'fr',
isPublicEmail: true,
isPublicGuilds: true
}
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
prismaMock.userSetting.update.mockResolvedValue({
...userSettingsExample,
...newSettings
})
const response = await request(application)
.put('/users/current/settings')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(401)
expect(response.body.errors.length).toEqual(1)
})
it('fails with invalid theme', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current/settings')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ theme: 'random theme value' })
.expect(400)
expect(response.body.errors.length).toEqual(1)
const { accessToken } = await authenticateUserTest()
const response = await application.inject({
method: 'PUT',
url: '/users/current/settings',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: newSettings
})
const responseJson = response.json()
expect(response.statusCode).toEqual(200)
expect(responseJson.settings.theme).toEqual(newSettings.theme)
expect(responseJson.settings.language).toEqual(newSettings.language)
expect(responseJson.settings.isPublicEmail).toEqual(
newSettings.isPublicEmail
)
expect(responseJson.settings.isPublicGuilds).toEqual(
newSettings.isPublicGuilds
)
})
it('fails with invalid language', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current/settings')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ language: 'random language value' })
.expect(400)
expect(response.body.errors.length).toEqual(1)
})
it('fails with invalid isPublicEmail', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/users/current/settings')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ isPublicEmail: 'not a boolean value' })
.expect(400)
expect(response.body.errors.length).toEqual(1)
const newSettings = {
language: 'somerandomlanguage'
}
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
const { accessToken } = await authenticateUserTest()
const response = await application.inject({
method: 'PUT',
url: '/users/current/settings',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: newSettings
})
expect(response.statusCode).toEqual(400)
})
})

View File

@ -1,7 +0,0 @@
import { Router } from 'express'
import { putCurrentSettingsRouter } from './put'
export const currentSettingsRouter = Router()
currentSettingsRouter.use('/', putCurrentSettingsRouter)

View File

@ -1,63 +1,71 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../../tools/middlewares/validateRequest'
import UserSetting, {
themes,
Theme,
languages,
Language
} from '../../../../models/UserSetting'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
import { onlyPossibleValuesValidation } from '../../../../tools/validations/onlyPossibleValuesValidation'
import prisma from '../../../../tools/database/prisma.js'
import { fastifyErrors } from '../../../../models/utils.js'
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { userSettingsSchema } from '../../../../models/UserSettings.js'
export const putCurrentSettingsRouter = Router()
const bodyPutServiceSchema = Type.Object({
theme: Type.Optional(userSettingsSchema.theme),
language: Type.Optional(userSettingsSchema.language),
isPublicEmail: Type.Optional(userSettingsSchema.isPublicEmail),
isPublicGuilds: Type.Optional(userSettingsSchema.isPublicGuilds)
})
putCurrentSettingsRouter.put(
'/users/current/settings',
authenticateUser,
[
body('isPublicEmail').optional({ nullable: true }).isBoolean(),
body('theme')
.optional({ nullable: true })
.trim()
.isString()
.custom(async (theme: Theme) => {
return await onlyPossibleValuesValidation([...themes], 'theme', theme)
}),
body('language')
.optional({ nullable: true })
.trim()
.isString()
.custom(async (language: Language) => {
return await onlyPossibleValuesValidation(
languages,
'language',
language
)
})
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const putServiceSchema: FastifySchema = {
description: 'Edit the current connected user settings',
tags: ['users'] as string[],
security: [
{
bearerAuth: []
}
const { isPublicEmail, theme, language } = req.body as {
isPublicEmail?: boolean
theme?: Theme
language?: Language
}
const user = req.user.current
const settings = await UserSetting.findOne({ where: { id: user.id } })
if (settings == null) {
throw new NotFoundError()
}
settings.isPublicEmail = isPublicEmail ?? settings.isPublicEmail
settings.theme = theme ?? settings.theme
settings.language = language ?? settings.language
await settings.save()
return res.status(200).json({ settings })
] as Array<{ [key: string]: [] }>,
body: bodyPutServiceSchema,
response: {
200: Type.Object({
settings: Type.Object(userSettingsSchema)
}),
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
500: fastifyErrors[500]
}
)
} as const
export const putCurrentUserSettings: FastifyPluginAsync = async (fastify) => {
await fastify.register(authenticateUser)
fastify.route<{
Body: BodyPutServiceSchemaType
}>({
method: 'PUT',
url: '/users/current/settings',
schema: putServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const { theme, language, isPublicEmail, isPublicGuilds } = request.body
const settings = await prisma.userSetting.findFirst({
where: { userId: request.user.current.id }
})
if (settings == null) {
throw fastify.httpErrors.internalServerError()
}
const newSettings = await prisma.userSetting.update({
where: { id: request.user.current.id },
data: {
theme: theme ?? settings.theme,
language: language ?? settings.language,
isPublicEmail: isPublicEmail ?? settings.isPublicEmail,
isPublicGuilds: isPublicGuilds ?? settings.isPublicGuilds
}
})
reply.statusCode = 200
return { settings: newSettings }
}
})
}