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,6 +0,0 @@
components:
securitySchemes:
bearerAuth:
type: 'http'
scheme: 'bearer'
bearerFormat: 'JWT'

View File

@ -1,87 +0,0 @@
definitions:
BadRequestError:
'400':
description: 'Bad Request'
content:
application/json:
schema:
type: 'object'
properties:
errors:
type: 'array'
items:
type: 'object'
properties:
message:
type: 'string'
field:
type: 'string'
required:
- 'message'
UnauthorizedError:
'401':
description: 'Unauthorized: Token is missing or invalid Bearer'
content:
application/json:
schema:
type: 'object'
properties:
errors:
type: 'array'
items:
type: 'object'
properties:
message:
type: 'string'
enum: ['Unauthorized: Token is missing or invalid Bearer']
ForbiddenError:
'403':
description: 'Forbidden'
content:
application/json:
schema:
type: 'object'
properties:
errors:
type: 'array'
items:
type: 'object'
properties:
message:
type: 'string'
enum: ['Forbidden']
NotFoundError:
'404':
description: 'Not Found'
content:
application/json:
schema:
type: 'object'
properties:
errors:
type: 'array'
items:
type: 'object'
properties:
message:
type: 'string'
enum: ['Not Found']
PayloadTooLargeError:
'413':
description: 'Payload Too Large'
content:
application/json:
schema:
type: 'object'
properties:
errors:
type: 'array'
items:
type: 'object'
properties:
message:
type: 'string'

View File

@ -1,20 +0,0 @@
definitions:
PaginateModel:
type: 'object'
properties:
hasMore:
type: 'boolean'
totalItems:
type: 'number'
itemsPerPage:
type: 'number'
page:
type: 'number'
PaginateModelParameters:
'parameters':
- name: 'itemsPerPage'
in: 'query'
required: false
- name: 'page'
in: 'query'
required: false

View File

@ -1,25 +0,0 @@
/channels/{channelId}:
delete:
security:
- bearerAuth: []
tags:
- 'channels'
summary: 'DELETE a channel with its id'
parameters:
- name: 'channelId'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
deletedChannelId:
type: 'number'

View File

@ -1,41 +0,0 @@
/channels/{channelId}:
put:
security:
- bearerAuth: []
tags:
- 'channels'
summary: 'UPDATE a channel with its id'
parameters:
- name: 'channelId'
in: 'path'
required: true
requestBody:
content:
application/json:
schema:
type: 'object'
properties:
name:
type: 'string'
minLength: 3
maxLength: 30
description:
type: 'string'
maxLength: 160
isDefault:
type: 'boolean'
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
channel:
allOf:
- $ref: '#/definitions/Channel'

View File

@ -1,71 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import Channel from '../../../../models/Channel'
import { errorsMessages } from '../delete'
import { createChannels } from '../../__test__/utils/createChannel'
describe('DELETE /channels/:channelId', () => {
it('succeeds and delete the channel', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToDelete = result.channels[0]
const response = await request(application)
.delete(`/channels/${channelToDelete.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.deletedChannelId).toEqual(channelToDelete.id)
const foundChannel = await Channel.findOne({
where: { id: channelToDelete.id }
})
expect(foundChannel).toBeNull()
})
it("fails if the channel doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.delete('/channels/23')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it('fails if the user is not the owner', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToDelete = result.channels[0]
const userToken = await authenticateUserTest()
const response = await request(application)
.delete(`/channels/${channelToDelete.id as number}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it("fails if it's the default channel", async () => {
const result = await createChannels([])
const defaultChannel = await Channel.findOne({
where: { guildId: result.guild.id as number, isDefault: true }
})
expect(defaultChannel).not.toBeNull()
const response = await request(application)
.delete(`/channels/${defaultChannel?.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([errorsMessages.channel.shouldNotBeTheDefault])
)
})
})

View File

@ -1,120 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import Channel from '../../../../models/Channel'
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
import { randomString } from '../../../../tools/utils/random'
import { createChannels } from '../../__test__/utils/createChannel'
describe('PUT /channels/:channelId', () => {
it('succeeds and edit name/description of the channel', async () => {
const name = 'general-updated'
const description = 'general-description'
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToEdit = result.channels[0]
const response = await request(application)
.put(`/channels/${channelToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name, description })
.expect(200)
expect(response.body.channel.name).toEqual(name)
expect(response.body.channel.description).toEqual(description)
})
it('succeeds and set default channel to true', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToEdit = result.channels[0]
const response = await request(application)
.put(`/channels/${channelToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ isDefault: true })
.expect(200)
const defaultChannels = await Channel.findAll({
where: { guildId: result.guild.id as number, isDefault: true }
})
expect(defaultChannels.length).toEqual(1)
expect(response.body.channel.name).toEqual(channel1.name)
expect(response.body.channel.isDefault).toBeTruthy()
})
it('succeeds with invalid slug name', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToEdit = result.channels[0]
const name = 'random channel'
const response = await request(application)
.put(`/channels/${channelToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name })
.expect(200)
expect(response.body.channel.name).toEqual(name)
expect(response.body.channel.isDefault).toBeFalsy()
})
it('fails with too long description', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToEdit = result.channels[0]
const response = await request(application)
.put(`/channels/${channelToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ description: randomString(170) })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('description', { max: 160 })
])
)
})
it('fails with too long name', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToEdit = result.channels[0]
const response = await request(application)
.put(`/channels/${channelToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name: ' random channel name ' + randomString(35) })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
])
)
})
it("fails if the channel doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/channels/23')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it('fails if the user is not the owner', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const channelToRemove = result.channels[0]
const userToken = await authenticateUserTest()
const response = await request(application)
.put(`/channels/${channelToRemove.id as number}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,56 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import Channel from '../../../models/Channel'
import Member from '../../../models/Member'
import { BadRequestError } from '../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import { deleteMessages } from '../../../tools/utils/deleteFiles'
import Message from '../../../models/Message'
import { emitToMembers } from '../../../tools/socket/emitEvents'
export const errorsMessages = {
channel: {
shouldNotBeTheDefault: 'The channel to delete should not be the default'
}
}
export const deleteByIdChannelsRouter = Router()
deleteByIdChannelsRouter.delete(
'/channels/:channelId',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { channelId } = req.params as { channelId: string }
const channel = await Channel.findOne({
where: { id: channelId },
include: [Message]
})
if (channel == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: channel.guildId, isOwner: true }
})
if (member == null) {
throw new NotFoundError()
}
if (channel.isDefault) {
throw new BadRequestError(errorsMessages.channel.shouldNotBeTheDefault)
}
const deletedChannelId = channel.id
await deleteMessages(channel.messages)
await channel.destroy()
await emitToMembers({
event: 'channels',
guildId: channel.guildId,
payload: { action: 'delete', item: channel }
})
return res.status(200).json({ deletedChannelId })
}
)

View File

@ -1,33 +0,0 @@
/channels/{channelId}/messages:
get:
security:
- bearerAuth: []
tags:
- 'messages'
summary: 'GET all the messages of a channel'
parameters:
- name: 'channelId'
in: 'path'
required: true
allOf:
- $ref: '#/definitions/PaginateModelParameters'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/PaginateModel'
type: 'object'
properties:
rows:
type: 'array'
items:
allOf:
- $ref: '#/definitions/Message'
- $ref: '#/definitions/User'

View File

@ -1,44 +0,0 @@
/channels/{channelId}/messages:
post:
security:
- bearerAuth: []
tags:
- 'messages'
summary: 'Create a new message'
parameters:
- name: 'channelId'
in: 'path'
required: true
requestBody:
content:
multipart/form-data:
schema:
type: 'object'
properties:
value:
type: 'string'
minLength: 1
maxLength: 50_000
type:
allOf:
- $ref: '#/definitions/MessageType'
file:
type: 'string'
format: 'binary'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- $ref: '#/definitions/PayloadTooLargeError'
- '201':
content:
application/json:
schema:
type: 'object'
properties:
message:
allOf:
- $ref: '#/definitions/Message'
- $ref: '#/definitions/User'

View File

@ -1,23 +0,0 @@
import request from 'supertest'
import application from '../../../../../application'
import { createMessages } from '../../../../messages/__test__/utils/createMessages'
describe('GET /channels/:channelId/messages', () => {
it('should get all the messages of the channel', async () => {
const messages = ['Hello world!', 'some random message']
const result = await createMessages(messages)
const response = await request(application)
.get(`/channels/${result.channelId}/messages`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.hasMore).toBeFalsy()
expect(response.body.totalItems).toEqual(messages.length)
expect(response.body.rows[0].value).toEqual(messages[0])
expect(response.body.rows[1].value).toEqual(messages[1])
expect(response.body.rows[1].user).not.toBeNull()
expect(response.body.rows[1].user.id).toEqual(result.user.id)
expect(response.body.rows[1].user.password).not.toBeDefined()
})
})

View File

@ -1,69 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../../__test__/utils/formatErrors'
import application from '../../../../../application'
import { createChannels } from '../../../__test__/utils/createChannel'
const channel1 = { name: 'general1', description: 'testing' }
describe('POST /channels/:channelId/messages', () => {
it('succeeds and create the message', async () => {
const value = 'my awesome message'
const result = await createChannels([channel1])
expect(result.channels.length).toEqual(1)
const channel = result.channels[0]
const response = await request(application)
.post(`/channels/${channel.id as number}/messages`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value, type: 'text' })
.expect(201)
expect(response.body.message).not.toBeNull()
expect(response.body.message.value).toEqual(value)
expect(response.body.message.type).toEqual('text')
expect(response.body.message.user).not.toBeNull()
expect(response.body.message.user.id).toEqual(result.user.id)
})
it('fails with empty message', async () => {
const result = await createChannels([channel1])
expect(result.channels.length).toEqual(1)
const channel = result.channels[0]
const response1 = await request(application)
.post(`/channels/${channel.id as number}/messages`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ type: 'text' })
.expect(400)
const response2 = await request(application)
.post(`/channels/${channel.id as number}/messages`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ type: 'file' })
.expect(400)
expect(response1.body.errors.length).toEqual(1)
expect(response2.body.errors.length).toEqual(1)
})
it("fails if the channel doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.post('/channels/2/messages')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ type: 'text', value: 'awesome' })
.expect(404)
expect(response.body.errors.length).toEqual(1)
})
it('fails if the user is not in the guild with this channel', async () => {
const result = await createChannels([channel1])
const channel = result.channels[0]
const userToken = await authenticateUserTest()
const response = await request(application)
.post(`/channels/${channel.id as number}/messages`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ value: 'some random message', type: 'text' })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,60 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import Channel from '../../../../models/Channel'
import Member from '../../../../models/Member'
import Message from '../../../../models/Message'
import { paginateModel } from '../../../../tools/database/paginateModel'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
import User from '../../../../models/User'
export const getMessagesRouter = Router()
getMessagesRouter.get(
'/channels/:channelId/messages',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { itemsPerPage, page } = req.query as {
itemsPerPage: string
page: string
}
const { channelId } = req.params as { channelId: string }
const user = req.user.current
const channel = await Channel.findOne({ where: { id: channelId } })
if (channel == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: channel.guildId }
})
if (member == null) {
throw new NotFoundError()
}
member.lastVisitedChannelId = channel.id
await member.save()
const result = await paginateModel({
Model: Message,
queryOptions: { itemsPerPage, page },
findOptions: {
order: [['createdAt', 'DESC']],
include: [{ model: Member, include: [User] }],
where: {
channelId: channel.id
}
}
})
return res.status(200).json({
hasMore: result.hasMore,
totalItems: result.totalItems,
itemsPerPage: result.itemsPerPage,
page: result.page,
rows: result.rows.reverse().map((row: any) => {
return { ...row.toJSON(), user: row.member.user.toJSON() }
})
})
}
)

View File

@ -1,9 +0,0 @@
import { Router } from 'express'
import { postMessagesRouter } from './post'
import { getMessagesRouter } from './get'
export const messagesChannelsRouter = Router()
messagesChannelsRouter.use('/', postMessagesRouter)
messagesChannelsRouter.use('/', getMessagesRouter)

View File

@ -1,122 +0,0 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import fileUpload from 'express-fileupload'
import { v4 as uuidv4 } from 'uuid'
import path from 'path'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../../tools/middlewares/validateRequest'
import Channel from '../../../../models/Channel'
import Member from '../../../../models/Member'
import Message, { MessageType, messageTypes } from '../../../../models/Message'
import {
commonErrorsMessages,
fileUploadOptions,
messagesFilePath,
tempPath
} from '../../../../tools/configurations/constants'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
import { onlyPossibleValuesValidation } from '../../../../tools/validations/onlyPossibleValuesValidation'
import { deleteAllFilesInDirectory } from '../../../../tools/utils/deleteFiles'
import { PayloadTooLargeError } from '../../../../tools/errors/PayloadTooLargeError'
import { BadRequestError } from '../../../../tools/errors/BadRequestError'
import { emitToMembers } from '../../../../tools/socket/emitEvents'
export const errorsMessages = {
type: {
shouldNotBeEmpty: 'Type should not be empty'
}
}
export const postMessagesRouter = Router()
postMessagesRouter.post(
'/channels/:channelId/messages',
authenticateUser,
fileUpload(fileUploadOptions),
[
body('value')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ min: 1, max: 50_000 })
.withMessage(
commonErrorsMessages.charactersLength('value', { min: 1, max: 50_000 })
),
body('type')
.notEmpty()
.withMessage(errorsMessages.type.shouldNotBeEmpty)
.trim()
.isString()
.custom(async (type: MessageType) => {
return await onlyPossibleValuesValidation(messageTypes, 'type', type)
})
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { value, type } = req.body as {
value?: string
type: MessageType
}
const file = req.files?.file
const { channelId } = req.params as { channelId: string }
const channel = await Channel.findOne({
where: { id: channelId, type: 'text' }
})
if (channel == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: channel.guildId }
})
if (member == null) {
throw new NotFoundError()
}
if (
(type === 'file' && file == null) ||
(type === 'text' && value == null)
) {
throw new BadRequestError("You can't send an empty message")
}
let filename: string | null = null
let mimetype = 'text/plain'
if (
value == null &&
type === 'file' &&
file != null &&
!Array.isArray(file)
) {
if (file.truncated) {
await deleteAllFilesInDirectory(tempPath)
throw new PayloadTooLargeError(
commonErrorsMessages.tooLargeFile('file')
)
}
mimetype = file.mimetype
const splitedMimetype = mimetype.split('/')
const fileExtension = splitedMimetype[1]
filename = `${uuidv4()}.${fileExtension}`
await file.mv(path.join(messagesFilePath.filePath, filename))
await deleteAllFilesInDirectory(tempPath)
}
const messageCreated = await Message.create({
value: filename != null ? `${messagesFilePath.name}/${filename}` : value,
type,
mimetype,
memberId: member.id,
channelId: channel.id
})
const message = { ...messageCreated.toJSON(), user: req.user.current }
await emitToMembers({
event: 'messages',
guildId: member.guildId,
payload: { action: 'create', item: message }
})
return res.status(201).json({ message })
}
)

View File

@ -1,92 +0,0 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../tools/middlewares/validateRequest'
import Channel from '../../../models/Channel'
import Member from '../../../models/Member'
import { commonErrorsMessages } from '../../../tools/configurations/constants'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import { emitToMembers } from '../../../tools/socket/emitEvents'
export const putByIdChannelsRouter = Router()
putByIdChannelsRouter.put(
'/channels/:channelId',
authenticateUser,
[
body('name')
.optional({ nullable: true })
.isString()
.trim()
.escape()
.isLength({ max: 30, min: 3 })
.withMessage(
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
),
body('description')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 160 })
.withMessage(
commonErrorsMessages.charactersLength('description', { max: 160 })
),
body('isDefault').optional({ nullable: true }).isBoolean()
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { channelId } = req.params as { channelId: string }
const { name, description, isDefault } = req.body as {
name?: string
description?: string
isDefault?: boolean
}
const channel = await Channel.findOne({
where: { id: channelId }
})
if (channel == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: channel.guildId, isOwner: true }
})
if (member == null) {
throw new NotFoundError()
}
channel.name = name ?? channel.name
channel.description = description ?? channel.description
if (isDefault != null) {
const defaultChannel = await Channel.findOne({
where: { isDefault: true, guildId: member.guildId }
})
if (isDefault && defaultChannel != null) {
defaultChannel.isDefault = false
channel.isDefault = true
await defaultChannel.save()
const defaultChannelMembers = await Member.findAll({
where: {
guildId: member.guildId,
lastVisitedChannelId: defaultChannel.id
}
})
for (const defaultChannelMember of defaultChannelMembers) {
defaultChannelMember.lastVisitedChannelId = channel.id
await defaultChannelMember.save()
}
}
}
await channel.save()
await emitToMembers({
event: 'channels',
guildId: channel.guildId,
payload: { action: 'update', item: channel }
})
return res.status(200).json({ channel })
}
)

View File

@ -1,24 +0,0 @@
definitions:
Channel:
type: 'object'
properties:
id:
type: 'integer'
description: 'Unique id'
name:
type: 'string'
type:
type: 'string'
enum: ['text', 'voice']
description:
type: 'string'
isDefault:
type: 'boolean'
guildId:
type: 'integer'
createdAt:
type: 'string'
format: 'date-time'
updatedAt:
type: 'string'
format: 'date-time'

View File

@ -1,42 +0,0 @@
import request from 'supertest'
import application from '../../../../application'
import Channel from '../../../../models/Channel'
import {
createGuild,
CreateGuildResult
} from '../../../guilds/__test__/utils/createGuild'
interface ChannelOptions {
name: string
description: string
}
interface CreateChannelsResult extends CreateGuildResult {
channels: Channel[]
}
export const createChannels = async (
channels: ChannelOptions[]
): Promise<CreateChannelsResult> => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const channelsResponses: Channel[] = []
for (const { name, description } of channels) {
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name, description })
.expect(201)
channelsResponses.push(response.body.channel)
}
return {
...result,
channels: channelsResponses
}
}

View File

@ -1,11 +0,0 @@
import { Router } from 'express'
import { deleteByIdChannelsRouter } from './[channelId]/delete'
import { messagesChannelsRouter } from './[channelId]/messages'
import { putByIdChannelsRouter } from './[channelId]/put'
export const channelsRouter = Router()
channelsRouter.use('/', deleteByIdChannelsRouter)
channelsRouter.use('/', putByIdChannelsRouter)
channelsRouter.use('/', messagesChannelsRouter)

View File

@ -1,8 +0,0 @@
import { Router } from 'express'
import swaggerUi from 'swagger-ui-express'
import { swaggerSpecification } from '../../tools/configurations/swaggerSpecification'
export const documentationRouter = Router()
documentationRouter.use('/documentation', swaggerUi.serve, swaggerUi.setup(swaggerSpecification))

View File

@ -1,24 +0,0 @@
/guilds/{guildId}:
delete:
security:
- bearerAuth: []
tags:
- 'guilds'
summary: 'DELETE a guild with its id'
parameters:
- name: 'guildId'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
deletedGuildId:
type: 'number'

View File

@ -1,25 +0,0 @@
/guilds/{guildId}:
get:
security:
- bearerAuth: []
tags:
- 'guilds'
summary: 'GET a guild with its id'
parameters:
- name: 'guildId'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
guild:
allOf:
- $ref: '#/definitions/Guild'

View File

@ -1,48 +0,0 @@
/guilds/{guildId}:
put:
security:
- bearerAuth: []
tags:
- 'guilds'
summary: 'Update a guild with its id'
parameters:
- name: 'guildId'
in: 'path'
required: true
requestBody:
content:
multipart/form-data:
schema:
type: 'object'
properties:
name:
type: 'string'
minLength: 3
maxLength: 30
description:
type: 'string'
maxLength: 160
icon:
type: 'string'
format: 'binary'
isPublic:
type: 'boolean'
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
guild:
allOf:
- $ref: '#/definitions/Guild'
type: 'object'
properties:
publicInvitation:
type: 'string'

View File

@ -1,62 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import Guild from '../../../../models/Guild'
import { createGuild } from '../../__test__/utils/createGuild'
describe('DELETE /guilds/:guildId', () => {
it('succeeds and delete the guild', async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.delete(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.deletedGuildId).toEqual(result.guild.id)
const foundGuild = await Guild.findOne({ where: { id: result?.guild.id as number } })
expect(foundGuild).toBeNull()
})
it("fails if the guild doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.delete('/guilds/23')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it("fails if the user isn't the owner", async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
const response = await request(application)
.delete(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,58 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import { createGuild } from '../../__test__/utils/createGuild'
describe('GET /guilds/:guildId', () => {
it('succeeds and get the guild', async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.get(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.guild.name).toEqual(name)
expect(response.body.guild.description).toEqual(description)
})
it("fails if the user isn't a member", async () => {
const result = await createGuild({
guild: { description: 'testing', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
const response = await request(application)
.get(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it("fails if the guild doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.get('/guilds/23')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,182 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import Guild from '../../../../models/Guild'
import Invitation from '../../../../models/Invitation'
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
import { randomString } from '../../../../tools/utils/random'
import { createGuild } from '../../__test__/utils/createGuild'
describe('PUT /guilds/:guildId', () => {
it('succeeds and edit the guild', async () => {
const name = 'guild'
const newName = 'guildtest'
const description = 'testing'
const newDescription = 'new description'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name: newName, description: newDescription })
.expect(200)
expect(response.body.guild.name).toEqual(newName)
expect(response.body.guild.description).toEqual(newDescription)
expect(response.body.guild.publicInvitation).toBeNull()
const foundGuild = await Guild.findOne({
where: { id: result?.guild.id as number }
})
expect(foundGuild?.name).toEqual(newName)
expect(foundGuild?.description).toEqual(newDescription)
})
it('succeeds and create/delete public invitations', async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const resIsPublic = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ isPublic: true })
.expect(200)
expect(resIsPublic.body.guild.isPublic).toBeTruthy()
expect(typeof resIsPublic.body.guild.publicInvitation).toBe('string')
const publicInvitation = await Invitation.findOne({
where: { isPublic: true, guildId: result?.guild.id as number }
})
expect(publicInvitation).not.toBeNull()
expect(publicInvitation?.expiresIn).toEqual(0)
const resIsNotPublic = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ isPublic: false })
.expect(200)
expect(resIsNotPublic.body.guild.isPublic).toBeFalsy()
expect(resIsNotPublic.body.guild.publicInvitation).toBeNull()
const notPublicInvitation = await Invitation.findOne({
where: { isPublic: false, guildId: result?.guild.id as number }
})
expect(notPublicInvitation).toBeNull()
})
it("fails if the user isn't the owner", async () => {
const name = 'guild'
const newName = 'guildtest'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
const response = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name: newName })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it("fails if the guild doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/guilds/23')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name: 'kjdjhdjh' })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it('fails with invalid name', async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name: randomString(35) })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
])
)
})
it('fails with name already used', async () => {
const { guild } = await createGuild({
guild: { description: 'testing', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const result = await createGuild({
guild: { description: 'testing', name: 'guild2' },
user: {
email: 'test@test2.com',
name: 'Test2'
}
})
const response = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name: guild.name })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Name already used']))
})
it('fails with invalid description', async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.put(`/guilds/${result.guild.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ description: randomString(165) })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('description', { max: 160 })
])
)
})
})

View File

@ -1,31 +0,0 @@
/guilds/{guildId}/channels:
get:
security:
- bearerAuth: []
tags:
- 'channels'
summary: 'GET all the channels of a guild'
parameters:
- name: 'guildId'
in: 'path'
required: true
allOf:
- $ref: '#/definitions/PaginateModelParameters'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/PaginateModel'
type: 'object'
properties:
rows:
type: 'array'
items:
allOf:
- $ref: '#/definitions/Channel'

View File

@ -1,39 +0,0 @@
/guilds/{guildId}/channels:
post:
security:
- bearerAuth: []
tags:
- 'channels'
summary: 'Create a channel'
parameters:
- name: 'guildId'
in: 'path'
required: true
requestBody:
content:
application/json:
schema:
type: 'object'
properties:
name:
type: 'string'
minLength: 3
maxLength: 30
description:
type: 'string'
maxLength: 160
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '201':
content:
application/json:
schema:
type: 'object'
properties:
channel:
allOf:
- $ref: '#/definitions/Channel'

View File

@ -1,23 +0,0 @@
import request from 'supertest'
import application from '../../../../../application'
import { createChannels } from '../../../../channels/__test__/utils/createChannel'
describe('GET /guilds/:guildId/channels', () => {
it('should get all the channels of the guild', async () => {
const channel1 = { name: 'general1', description: 'testing' }
const channel2 = { name: 'general2', description: 'testing' }
const result = await createChannels([channel1, channel2])
const response = await request(application)
.get(`/guilds/${result.guild.id as number}/channels/`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.hasMore).toBeFalsy()
expect(response.body.rows.length).toEqual(3)
expect(response.body.rows[0].name).toEqual(channel2.name)
expect(response.body.rows[0].description).toEqual(channel2.description)
expect(response.body.rows[1].name).toEqual(channel1.name)
expect(response.body.rows[1].description).toEqual(channel1.description)
})
})

View File

@ -1,146 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../../__test__/utils/formatErrors'
import application from '../../../../../application'
import { commonErrorsMessages } from '../../../../../tools/configurations/constants'
import { randomString } from '../../../../../tools/utils/random'
import { createGuild } from '../../../__test__/utils/createGuild'
import { errorsMessages } from '../post'
describe('POST /guilds/:guildId/channels', () => {
it('succeeds with valid name/description', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const name = 'channel-name'
const description = 'testing channel creation'
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name, description })
.expect(201)
expect(response.body.channel).not.toBeNull()
expect(response.body.channel.guildId).not.toBeUndefined()
expect(response.body.channel.name).toBe(name)
expect(response.body.channel.description).toBe(description)
})
it('succeeds with only channel name', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const name = 'channel-name'
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name })
.expect(201)
expect(response.body.channel).not.toBeNull()
expect(response.body.channel.name).toBe(name)
})
it('succeeds with invalid slug name', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const name = 'channel name'
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name, description: 'testing' })
.expect(201)
expect(response.body.channel).not.toBeNull()
expect(response.body.channel.name).toBe(name)
})
it('fails without name', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ description: 'testing channel creation' })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(3)
expect(errors).toEqual(
expect.arrayContaining([
errorsMessages.name.isRequired,
commonErrorsMessages.charactersLength('name', { min: 3, max: 30 })
])
)
})
it('fails with invalid description', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ name: 'channel-name', description: randomString(170) })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('description', { max: 160 })
])
)
})
it("fails if the user isn't the owner", async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
const name = 'channel-name'
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/channels`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name, description: 'testing channel creation' })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it("fails if the guild does't exist", async () => {
const userToken = await authenticateUserTest()
const name = 'channel-name'
const response = await request(application)
.post('/guilds/1/channels')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name, description: 'testing channel creation' })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,43 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import Channel from '../../../../models/Channel'
import Member from '../../../../models/Member'
import { paginateModel } from '../../../../tools/database/paginateModel'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
export const getChannelsRouter = Router()
getChannelsRouter.get(
'/guilds/:guildId/channels',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { itemsPerPage, page } = req.query as {
itemsPerPage: string
page: string
}
const user = req.user.current
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId }
})
if (member == null) {
throw new NotFoundError()
}
const channels = await paginateModel({
Model: Channel,
queryOptions: { itemsPerPage, page },
findOptions: {
order: [['createdAt', 'DESC']],
where: {
guildId: member.guildId
}
}
})
return res.status(200).json(channels)
}
)

View File

@ -1,9 +0,0 @@
import { Router } from 'express'
import { getChannelsRouter } from './get'
import { postChannelsRouter } from './post'
export const guildsChannelsRouter = Router()
guildsChannelsRouter.use('/', getChannelsRouter)
guildsChannelsRouter.use('/', postChannelsRouter)

View File

@ -1,73 +0,0 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../../tools/middlewares/validateRequest'
import Channel from '../../../../models/Channel'
import Member from '../../../../models/Member'
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
import { emitToMembers } from '../../../../tools/socket/emitEvents'
export const errorsMessages = {
name: {
isRequired: 'Name is required'
}
}
export const postChannelsRouter = Router()
postChannelsRouter.post(
'/guilds/:guildId/channels',
authenticateUser,
[
body('name')
.notEmpty()
.withMessage(errorsMessages.name.isRequired)
.isString()
.trim()
.escape()
.isLength({ max: 30, min: 3 })
.withMessage(
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
),
body('description')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 160 })
.withMessage(
commonErrorsMessages.charactersLength('description', { max: 160 })
)
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { name, description = '' } = req.body as {
name: string
description?: string
}
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId, isOwner: true }
})
if (member == null) {
throw new NotFoundError()
}
const channel = await Channel.create({
name,
description,
guildId: member.guildId
})
await emitToMembers({
event: 'channels',
guildId: member.guildId,
payload: { action: 'create', item: channel }
})
return res.status(201).json({ channel })
}
)

View File

@ -1,57 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import Guild from '../../../models/Guild'
import Member from '../../../models/Member'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import { guildsIconPath } from '../../../tools/configurations/constants'
import { deleteFile, deleteMessages } from '../../../tools/utils/deleteFiles'
import Channel from '../../../models/Channel'
import Message from '../../../models/Message'
import { emitToMembers } from '../../../tools/socket/emitEvents'
export const deleteByIdGuildsRouter = Router()
deleteByIdGuildsRouter.delete(
'/guilds/:guildId',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId, isOwner: true },
include: [Guild]
})
if (member == null) {
throw new NotFoundError()
}
const deletedGuildId = member.guild.id
await emitToMembers({
event: 'guilds',
guildId: member.guildId,
payload: { action: 'delete', item: member.guild }
})
await deleteFile({
basePath: guildsIconPath,
valueSavedInDatabase: member.guild.icon
})
const members = await Member.findAll({ where: { guildId: deletedGuildId } })
for (const member of members) {
await member.destroy()
}
const channels = await Channel.findAll({
where: { guildId },
include: [Message]
})
for (const channel of channels) {
await deleteMessages(channel.messages)
await channel.destroy()
}
await member.guild.destroy()
return res.status(200).json({ deletedGuildId })
}
)

View File

@ -1,29 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import Guild from '../../../models/Guild'
import Member from '../../../models/Member'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
export const getByIdGuildsRouter = Router()
getByIdGuildsRouter.get(
'/guilds/:guildId',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId },
include: [Guild]
})
if (member == null) {
throw new NotFoundError()
}
return res.status(200).json({ guild: member.guild })
}
)

View File

@ -1,19 +0,0 @@
import { Router } from 'express'
import { deleteByIdGuildsRouter } from './delete'
import { getByIdGuildsRouter } from './get'
import { putByIdGuildsRouter } from './put'
import { guildsChannelsRouter } from './channels'
import { guildsInvitationsRouter } from './invitations'
import { guildsMembersRouter } from './members'
export const guildsGetByIdRouter = Router()
guildsGetByIdRouter.use('/', getByIdGuildsRouter)
guildsGetByIdRouter.use('/', deleteByIdGuildsRouter)
guildsGetByIdRouter.use('/', putByIdGuildsRouter)
guildsGetByIdRouter.use('/', guildsChannelsRouter)
guildsGetByIdRouter.use('/', guildsInvitationsRouter)
guildsGetByIdRouter.use('/', guildsMembersRouter)

View File

@ -1,31 +0,0 @@
/guilds/{guildId}/invitations:
get:
security:
- bearerAuth: []
tags:
- 'invitations'
summary: 'GET all the invitations of a guild'
parameters:
- name: 'guildId'
in: 'path'
required: true
allOf:
- $ref: '#/definitions/PaginateModelParameters'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/PaginateModel'
type: 'object'
properties:
rows:
type: 'array'
items:
allOf:
- $ref: '#/definitions/Invitation'

View File

@ -1,40 +0,0 @@
/guilds/{guildId}/invitations:
post:
security:
- bearerAuth: []
tags:
- 'invitations'
summary: 'Create an invitation'
parameters:
- name: 'guildId'
in: 'path'
required: true
requestBody:
content:
application/json:
schema:
type: 'object'
properties:
value:
type: 'string'
minLength: 1
maxLength: 250
expiresIn:
type: 'integer'
isPublic:
type: 'boolean'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- '201':
content:
application/json:
schema:
type: 'object'
properties:
invitation:
allOf:
- $ref: '#/definitions/Invitation'

View File

@ -1,46 +0,0 @@
import request from 'supertest'
import application from '../../../../../application'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../../__test__/utils/formatErrors'
import { createInvitation } from '../../../../invitations/__test__/utils/createInvitation'
describe('GET /guilds/:guildId/invitations', () => {
it('should get all the invitations of the guild', async () => {
const value1 = 'awesome'
const value2 = 'awesomevalue'
const result = await createInvitation({ value: value1 })
await createInvitation({
value: value2,
guildId: result?.guild.id
})
const response = await request(application)
.get(`/guilds/${result?.guild.id as number}/invitations`)
.set(
'Authorization',
`${result?.user.type as string} ${result?.user.accessToken as string}`
)
.send()
.expect(200)
expect(response.body.hasMore).toBeFalsy()
expect(response.body.rows.length).toEqual(2)
expect(response.body.rows[0].value).toEqual(value2)
expect(response.body.rows[1].value).toEqual(value1)
})
it('fails if the user is not the owner', async () => {
const userToken = await authenticateUserTest()
const result = await createInvitation()
const response = await request(application)
.get(`/guilds/${result?.guild.id as number}/invitations`)
.set(
'Authorization',
`${userToken.type as string} ${userToken.accessToken}`
)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,163 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../../__test__/utils/formatErrors'
import application from '../../../../../application'
import { createGuild } from '../../../__test__/utils/createGuild'
import { errorsMessages } from '../post'
import { commonErrorsMessages } from '../../../../../tools/configurations/constants'
describe('POST /guilds/:guildId/invitations', () => {
it('succeeds and create the invitation', async () => {
const value = 'random'
const expiresIn = 0
const isPublic = false
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value, expiresIn, isPublic })
.expect(201)
expect(response.body.invitation.value).toEqual(value)
expect(response.body.invitation.expiresIn).toEqual(expiresIn)
expect(response.body.invitation.isPublic).toEqual(isPublic)
})
it('fails with empty value', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ expiresIn: 0 })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(3)
expect(errors).toEqual(
expect.arrayContaining([
errorsMessages.value.shouldNotBeEmpty,
errorsMessages.value.mustBeSlug,
commonErrorsMessages.charactersLength('value', { max: 250, min: 1 })
])
)
})
it('fails with invalid slug value', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value: 'random value' })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([errorsMessages.value.mustBeSlug])
)
})
it('fails with negative expiresIn', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value: 'awesome', expiresIn: -42 })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([errorsMessages.expiresIn.mustBeGreaterOrEqual])
)
})
it('fails if the invitation slug value already exists', async () => {
const value = 'awesome'
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value })
.expect(201)
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Value already used']))
})
it('fails with isPublic: true - if there is already a public invitation for this guild', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value: 'awesome', isPublic: true })
.expect(201)
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value: 'awesome2', isPublic: true })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([errorsMessages.public.alreadyHasInvitation])
)
})
it('fails if the user is not the owner', async () => {
const userToken = await authenticateUserTest()
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.post(`/guilds/${result.guild.id as number}/invitations`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ value: 'value' })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,43 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import Invitation from '../../../../models/Invitation'
import Member from '../../../../models/Member'
import { paginateModel } from '../../../../tools/database/paginateModel'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
export const getInvitationsRouter = Router()
getInvitationsRouter.get(
'/guilds/:guildId/invitations',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { itemsPerPage, page } = req.query as {
itemsPerPage: string
page: string
}
const user = req.user.current
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId, isOwner: true }
})
if (member == null) {
throw new NotFoundError()
}
const invitations = await paginateModel({
Model: Invitation,
queryOptions: { itemsPerPage, page },
findOptions: {
order: [['createdAt', 'DESC']],
where: {
guildId: member.guildId
}
}
})
return res.status(200).json(invitations)
}
)

View File

@ -1,9 +0,0 @@
import { Router } from 'express'
import { postInvitationsRouter } from './post'
import { getInvitationsRouter } from './get'
export const guildsInvitationsRouter = Router()
guildsInvitationsRouter.use('/', postInvitationsRouter)
guildsInvitationsRouter.use('/', getInvitationsRouter)

View File

@ -1,89 +0,0 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../../tools/middlewares/validateRequest'
import Invitation from '../../../../models/Invitation'
import Member from '../../../../models/Member'
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
import { alreadyUsedValidation } from '../../../../tools/validations/alreadyUsedValidation'
import { BadRequestError } from '../../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
export const errorsMessages = {
value: {
mustBeSlug: 'Value must be a slug',
shouldNotBeEmpty: 'Value should not be empty'
},
expiresIn: {
mustBeGreaterOrEqual: 'ExpiresIn must be >= 0'
},
public: {
alreadyHasInvitation: 'There is already a public invitation for this guild'
}
}
export const postInvitationsRouter = Router()
postInvitationsRouter.post(
'/guilds/:guildId/invitations',
authenticateUser,
[
body('value')
.notEmpty()
.withMessage(errorsMessages.value.shouldNotBeEmpty)
.trim()
.escape()
.isLength({ max: 250, min: 1 })
.withMessage(
commonErrorsMessages.charactersLength('value', { max: 250, min: 1 })
)
.isSlug()
.withMessage(errorsMessages.value.mustBeSlug)
.custom(async (value: string) => {
return await alreadyUsedValidation(Invitation, 'value', value)
}),
body('expiresIn')
.optional({ nullable: true })
.isInt({ min: 0 })
.withMessage(errorsMessages.expiresIn.mustBeGreaterOrEqual),
body('isPublic').optional({ nullable: true }).isBoolean()
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { value, expiresIn = 0, isPublic = false } = req.body as {
value: string
expiresIn?: number
isPublic?: boolean
}
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId, isOwner: true }
})
if (member == null) {
throw new NotFoundError()
}
const foundInvitation = await Invitation.findOne({
where: { isPublic: true, guildId: member.guildId }
})
if (isPublic && foundInvitation != null) {
throw new BadRequestError(errorsMessages.public.alreadyHasInvitation)
}
let expiresInValue = expiresIn
if (expiresInValue > 0) {
expiresInValue += Date.now()
}
const invitation = await Invitation.create({
value,
expiresIn,
isPublic,
guildId: member.guildId
})
return res.status(201).json({ invitation })
}
)

View File

@ -1,32 +0,0 @@
/guilds/{guildId}/members:
get:
security:
- bearerAuth: []
tags:
- 'members'
summary: 'GET all the members of a guild'
parameters:
- name: 'guildId'
in: 'path'
required: true
allOf:
- $ref: '#/definitions/PaginateModelParameters'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/PaginateModel'
type: 'object'
properties:
rows:
type: 'array'
items:
allOf:
- $ref: '#/definitions/Member'
- $ref: '#/definitions/User'

View File

@ -1,35 +0,0 @@
import request from 'supertest'
import application from '../../../../../application'
import Member from '../../../../../models/Member'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import { createGuild } from '../../../__test__/utils/createGuild'
describe('GET /guilds/:guildId/members', () => {
it('should get all the members of a guild', async () => {
const result = await createGuild({
guild: { description: 'description', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
await Member.create({
userId: userToken.userId,
guildId: result.guild.id,
lastVisitedChannelId: 1
})
const response = await request(application)
.get(`/guilds/${result.guild.id as number}/members`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.hasMore).toBeFalsy()
expect(response.body.totalItems).toEqual(2)
expect(response.body.rows[0].guildId).toEqual(result.guild.id)
expect(response.body.rows[1].guildId).toEqual(result.guild.id)
expect(response.body.rows[1].user).not.toBeNull()
expect(response.body.rows[1].user.password).not.toBeDefined()
})
})

View File

@ -1,50 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import Member from '../../../../models/Member'
import { paginateModel } from '../../../../tools/database/paginateModel'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
export const getMembersRouter = Router()
getMembersRouter.get(
'/guilds/:guildId/members',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { itemsPerPage, page } = req.query as {
itemsPerPage: string
page: string
}
const user = req.user.current
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId }
})
if (member == null) {
throw new NotFoundError()
}
const result = await paginateModel({
Model: Member,
queryOptions: { itemsPerPage, page },
findOptions: {
order: [['createdAt', 'DESC']],
where: {
guildId: member.guildId
}
}
})
return res.status(200).json({
hasMore: result.hasMore,
totalItems: result.totalItems,
itemsPerPage: result.itemsPerPage,
page: result.page,
rows: result.rows.map((row) => {
return { ...row.toJSON(), user: user.toJSON() }
})
})
}
)

View File

@ -1,7 +0,0 @@
import { Router } from 'express'
import { getMembersRouter } from './get'
export const guildsMembersRouter = Router()
guildsMembersRouter.use('/', getMembersRouter)

View File

@ -1,121 +0,0 @@
import { Request, Response, Router } from 'express'
import fileUpload from 'express-fileupload'
import { body } from 'express-validator'
import { v4 as uuidv4 } from 'uuid'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../tools/middlewares/validateRequest'
import Guild from '../../../models/Guild'
import Invitation from '../../../models/Invitation'
import Member from '../../../models/Member'
import { ObjectAny } from '../../../typings/utils'
import {
commonErrorsMessages,
guildsIconPath,
imageFileUploadOptions
} from '../../../tools/configurations/constants'
import { alreadyUsedValidation } from '../../../tools/validations/alreadyUsedValidation'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import { uploadImage } from '../../../tools/utils/uploadImage'
import { emitToMembers } from '../../../tools/socket/emitEvents'
export const putByIdGuildsRouter = Router()
putByIdGuildsRouter.put(
'/guilds/:guildId',
authenticateUser,
fileUpload(imageFileUploadOptions),
[
body('name')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 30, min: 3 })
.withMessage(
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
)
.custom(async (name: string) => {
return await alreadyUsedValidation(Guild, 'name', name)
}),
body('description')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 160 })
.withMessage(
commonErrorsMessages.charactersLength('description', { max: 160 })
),
body('isPublic')
.optional({ nullable: true })
.isBoolean()
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { name, description, isPublic } = req.body as {
name?: string
description?: string
isPublic?: boolean
}
const icon = req.files?.icon
const user = req.user.current
const { guildId } = req.params as { guildId: string }
const member = await Member.findOne({
where: { userId: user.id, guildId, isOwner: true },
include: [{ model: Guild, include: [Invitation] }]
})
if (member == null) {
throw new NotFoundError()
}
let invitation = member.guild.invitations.find(
invitation => invitation.isPublic
)
if (isPublic != null) {
if (isPublic && !member.guild.isPublic) {
invitation = await Invitation.create({
isPublic: true,
guildId: member.guild.id,
expiresIn: 0,
value: uuidv4()
})
member.guild.isPublic = true
} else if (!isPublic) {
const foundInvitation = await Invitation.findOne({
where: { isPublic: true, guildId: member.guild.id }
})
if (foundInvitation != null) {
await foundInvitation.destroy()
}
member.guild.isPublic = false
invitation = undefined
}
}
member.guild.name = name ?? member.guild.name
member.guild.description = description ?? member.guild.description
const resultUpload = await uploadImage({
image: icon,
propertyName: 'icon',
oldImage: member.guild.icon,
imagesPath: guildsIconPath.filePath
})
if (resultUpload != null) {
member.guild.icon = `${guildsIconPath.name}/${resultUpload}`
}
await member.guild.save()
const guild = member.guild.toJSON() as ObjectAny
guild.publicInvitation = invitation != null ? invitation.value : null
delete guild.invitations
await emitToMembers({
event: 'guilds',
guildId: guild.id,
payload: { action: 'update', item: guild }
})
return res.status(200).json({ guild })
}
)

View File

@ -1,22 +0,0 @@
definitions:
Guild:
type: 'object'
properties:
id:
type: 'integer'
description: 'Unique id'
name:
type: 'string'
description: 'Unique name'
description:
type: 'string'
icon:
type: 'string'
isPublic:
type: 'boolean'
createdAt:
type: 'string'
format: 'date-time'
updatedAt:
type: 'string'
format: 'date-time'

View File

@ -1,32 +0,0 @@
/guilds:
get:
security:
- bearerAuth: []
tags:
- 'guilds'
summary: 'GET all the guilds of a Member'
allOf:
- $ref: '#/definitions/PaginateModelParameters'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/PaginateModel'
type: 'object'
properties:
rows:
type: 'array'
items:
allOf:
- $ref: '#/definitions/Member'
type: 'object'
properties:
guild:
allOf:
- $ref: '#/definitions/Guild'

View File

@ -1,37 +0,0 @@
/guilds:
post:
security:
- bearerAuth: []
tags:
- 'guilds'
summary: 'Create a guild'
requestBody:
content:
multipart/form-data:
schema:
type: 'object'
properties:
name:
type: 'string'
minLength: 3
maxLength: 30
description:
type: 'string'
maxLength: 160
icon:
type: 'string'
format: 'binary'
responses:
allOf:
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- '201':
content:
application/json:
schema:
type: 'object'
properties:
guild:
allOf:
- $ref: '#/definitions/Guild'

View File

@ -1,28 +0,0 @@
import request from 'supertest'
import application from '../../../application'
import { createGuild } from './utils/createGuild'
describe('GET /guilds', () => {
it('should get all the guild of the member', async () => {
const name = 'guild'
const description = 'testing'
const guild = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const response = await request(application)
.get('/guilds')
.set('Authorization', `${guild.user.type} ${guild.user.accessToken}`)
.send()
.expect(200)
expect(response.body.hasMore).toBeFalsy()
expect(response.body.rows.length).toEqual(1)
expect(response.body.rows[0].isOwner).toBeTruthy()
expect(response.body.rows[0].guild.name).toEqual(name)
expect(response.body.rows[0].guild.description).toEqual(description)
})
})

View File

@ -1,72 +1,58 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../__test__/utils/formatErrors'
import application from '../../../application'
import { commonErrorsMessages } from '../../../tools/configurations/constants'
import { randomString } from '../../../tools/utils/random'
import { application } from '../../../application.js'
import { authenticateUserTest } from '../../../__test__/utils/authenticateUserTest.js'
import { prismaMock } from '../../../__test__/setup.js'
import { guildExample } from '../../../models/Guild.js'
import { memberExample } from '../../../models/Member.js'
import { channelExample } from '../../../models/Channel.js'
import { userExample } from '../../../models/User.js'
describe('POST /guilds', () => {
it('succeeds with valid name/description', async () => {
const name = 'Test'
const userToken = await authenticateUserTest()
const response = await request(application)
.post('/guilds')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name, description: 'testing guild creation' })
.expect(201)
expect(response.body.guild).not.toBeNull()
expect(response.body.guild.name).toBe(name)
it('succeeds', async () => {
prismaMock.guild.create.mockResolvedValue(guildExample)
prismaMock.member.create.mockResolvedValue(memberExample)
prismaMock.member.findUnique.mockResolvedValue({
...memberExample,
...userExample
})
prismaMock.channel.create.mockResolvedValue(channelExample)
const { accessToken, user } = await authenticateUserTest()
const response = await application.inject({
method: 'POST',
url: '/guilds',
headers: {
authorization: `Bearer ${accessToken}`
},
payload: {
name: guildExample.name,
description: guildExample.description
}
})
const responseJson = response.json()
expect(response.statusCode).toEqual(201)
expect(responseJson.guild.id).toEqual(guildExample.id)
expect(responseJson.guild.name).toEqual(guildExample.name)
expect(responseJson.guild.members.length).toEqual(1)
expect(responseJson.guild.members[0].userId).toEqual(user.id)
expect(responseJson.guild.members[0].user.name).toEqual(user.name)
expect(responseJson.guild.members[0].guildId).toEqual(guildExample.id)
expect(responseJson.guild.members[0].isOwner).toEqual(memberExample.isOwner)
expect(responseJson.guild.channels.length).toEqual(1)
expect(responseJson.guild.channels[0].id).toEqual(channelExample.id)
expect(responseJson.guild.channels[0].guildId).toEqual(guildExample.id)
})
it('fails with invalid name', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.post('/guilds')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name: randomString(35), description: 'testing guild creation' })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
])
)
})
it('fails with name already used', async () => {
const userToken = await authenticateUserTest()
const name = 'guild'
const response1 = await request(application)
.post('/guilds')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name, description: 'testing guild creation' })
.expect(201)
expect(response1.body.guild.name).toBe(name)
const response2 = await request(application)
.post('/guilds')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name, description: 'testing guild creation' })
.expect(400)
const errors = formatErrors(response2.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Name already used']))
})
it('fails with invalid description', async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.post('/guilds')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name: 'Test', description: randomString(165) })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual([
commonErrorsMessages.charactersLength('description', { max: 160 })
])
it('fails with empty name and description', async () => {
prismaMock.guild.create.mockResolvedValue(guildExample)
prismaMock.member.create.mockResolvedValue(memberExample)
prismaMock.channel.create.mockResolvedValue(channelExample)
const { accessToken } = await authenticateUserTest()
const response = await application.inject({
method: 'POST',
url: '/guilds',
headers: {
authorization: `Bearer ${accessToken}`
}
})
expect(response.statusCode).toEqual(400)
})
})

View File

@ -1,84 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import application from '../../../../application'
import Member from '../../../../models/Member'
interface CreateGuildOptions {
user: {
email?: string
name?: string
tokenResponse?: {
userId: number
accessToken: string
type: 'Bearer'
}
}
guild: {
name: string
description: string
shouldBePublic?: boolean
}
}
export interface CreateGuildResult {
user: {
id: number
accessToken: string
type: 'Bearer'
}
guild: {
id?: number
name: string
description: string
icon: string
isPublic: boolean
}
}
export const createGuild = async (
options: CreateGuildOptions
): Promise<CreateGuildResult> => {
const { user, guild } = options
let userToken = { type: 'Bearer', accessToken: '', userId: 1 }
if (user.email != null && user.name != null) {
userToken = await authenticateUserTest({
email: user.email,
name: user.name,
shouldBeConfirmed: true
})
}
if (user.tokenResponse != null) {
userToken = user.tokenResponse
}
let response = await request(application)
.post('/guilds')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ name: guild.name, description: guild.description })
.expect(201)
expect(response.body.guild).not.toBeNull()
const member = await Member.findOne({ where: { userId: userToken.userId } })
expect(member).not.toBeNull()
expect(member?.isOwner).toBeTruthy()
expect(member?.guildId).toEqual(response.body.guild.id)
if (member == null) {
throw new Error('"member" should not be null')
}
if (guild.shouldBePublic != null && guild.shouldBePublic) {
response = await request(application)
.put(`/guilds/${response.body.guild.id as string}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ isPublic: true })
.expect(200)
}
return {
user: {
id: userToken.userId,
accessToken: userToken.accessToken,
type: 'Bearer'
},
guild: { ...response.body.guild }
}
}

View File

@ -1,36 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../tools/middlewares/authenticateUser'
import Guild from '../../models/Guild'
import Member from '../../models/Member'
import { paginateModel } from '../../tools/database/paginateModel'
import { ForbiddenError } from '../../tools/errors/ForbiddenError'
export const getGuildsRouter = Router()
getGuildsRouter.get(
'/guilds',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const { itemsPerPage, page } = req.query as {
itemsPerPage: string
page: string
}
const user = req.user.current
const guilds = await paginateModel({
Model: Member,
queryOptions: { itemsPerPage, page },
findOptions: {
order: [['createdAt', 'DESC']],
where: {
userId: user.id
},
include: [Guild]
}
})
return res.status(200).json(guilds)
}
)

View File

@ -1,13 +1,7 @@
import { Router } from 'express'
import { FastifyPluginAsync } from 'fastify'
import { getGuildsRouter } from './get'
import { getPublicDiscoverGuildsRouter } from './public/discover/get'
import { postGuildsRouter } from './post'
import { guildsGetByIdRouter } from './[guildId]'
import { postGuilds } from './post.js'
export const guildsRouter = Router()
guildsRouter.use('/', postGuildsRouter)
guildsRouter.use('/', getGuildsRouter)
guildsRouter.use('/', getPublicDiscoverGuildsRouter)
guildsRouter.use('/', guildsGetByIdRouter)
export const guildsService: FastifyPluginAsync = async (fastify) => {
await fastify.register(postGuilds)
}

View File

@ -1,81 +1,94 @@
import { Request, Response, Router } from 'express'
import fileUpload from 'express-fileupload'
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 Channel from '../../models/Channel'
import Guild from '../../models/Guild'
import Member from '../../models/Member'
import {
commonErrorsMessages,
guildsIconPath,
imageFileUploadOptions
} from '../../tools/configurations/constants'
import { alreadyUsedValidation } from '../../tools/validations/alreadyUsedValidation'
import { ForbiddenError } from '../../tools/errors/ForbiddenError'
import { uploadImage } from '../../tools/utils/uploadImage'
import prisma from '../../tools/database/prisma.js'
import { fastifyErrors } from '../../models/utils.js'
import authenticateUser from '../../tools/plugins/authenticateUser.js'
import { guildSchema } from '../../models/Guild.js'
import { channelSchema } from '../../models/Channel.js'
import { memberSchema } from '../../models/Member.js'
import { userPublicSchema } from '../../models/User.js'
export const postGuildsRouter = Router()
const bodyPostServiceSchema = Type.Object({
name: guildSchema.name,
description: guildSchema.description
})
postGuildsRouter.post(
'/guilds',
authenticateUser,
fileUpload(imageFileUploadOptions),
[
body('name')
.trim()
.escape()
.notEmpty()
.isLength({ max: 30, min: 3 })
.withMessage(
commonErrorsMessages.charactersLength('name', { max: 30, min: 3 })
)
.custom(async (name: string) => {
return await alreadyUsedValidation(Guild, 'name', name)
}),
body('description')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 160 })
.withMessage(
commonErrorsMessages.charactersLength('description', { max: 160 })
)
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const postServiceSchema: FastifySchema = {
description: 'Create a guild.',
tags: ['guilds'] as string[],
security: [
{
bearerAuth: []
}
const { name, description = '' } = req.body as {
name: string
description?: string
}
const icon = req.files?.icon
const user = req.user.current
const resultUpload = await uploadImage({
image: icon,
propertyName: 'icon',
oldImage: `${guildsIconPath.name}/default.png`,
imagesPath: guildsIconPath.filePath
})
const guild = await Guild.create({ name, description })
const channel = await Channel.create({
name: 'general',
isDefault: true,
guildId: guild.id
})
await Member.create({
userId: user.id,
guildId: guild.id,
isOwner: true,
lastVisitedChannelId: channel.id
})
if (resultUpload != null) {
guild.icon = `${guildsIconPath.name}/${resultUpload}`
await guild.save()
}
return res.status(201).json({ guild })
] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema,
response: {
201: Type.Object({
guild: Type.Object({
...guildSchema,
channels: Type.Array(Type.Object(channelSchema)),
members: Type.Array(
Type.Object({
...memberSchema,
user: Type.Object(userPublicSchema)
})
)
})
}),
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
500: fastifyErrors[500]
}
)
} as const
export const postGuilds: FastifyPluginAsync = async (fastify) => {
await fastify.register(authenticateUser)
fastify.route<{
Body: BodyPostServiceSchemaType
}>({
method: 'POST',
url: '/guilds',
schema: postServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const { name, description } = request.body
const guild = await prisma.guild.create({ data: { name, description } })
const channel = await prisma.channel.create({
data: { name: 'general', guildId: guild.id }
})
const memberCreated = await prisma.member.create({
data: {
userId: request.user.current.id,
isOwner: true,
guildId: guild.id
}
})
const members = await Promise.all(
[memberCreated].map(async (member) => {
const user = await prisma.user.findUnique({
where: { id: member?.userId }
})
return {
...member,
user
}
})
)
reply.statusCode = 201
return {
guild: {
...guild,
channels: [channel],
members
}
}
}
})
}

View File

@ -1,35 +0,0 @@
/guilds/public/discover:
get:
security:
- bearerAuth: []
tags:
- 'guilds'
summary: 'GET all the public guilds'
allOf:
- $ref: '#/definitions/PaginateModelParameters'
parameters:
- name: 'search'
in: 'query'
required: false
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/PaginateModel'
type: 'object'
properties:
rows:
type: 'array'
items:
allOf:
- $ref: '#/definitions/Guild'
type: 'object'
properties:
publicInvitation:
type: 'string'

View File

@ -1,30 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import application from '../../../../../application'
import { createGuild } from '../../../__test__/utils/createGuild'
describe('GET /guilds/public/discover', () => {
it('should get all the public guilds', async () => {
const name = 'guild'
const description = 'testing'
await createGuild({
guild: { description, name, shouldBePublic: true },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
const response = await request(application)
.get('/guilds/public/discover')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(200)
expect(response.body.hasMore).toBeFalsy()
expect(response.body.rows.length).toEqual(1)
expect(response.body.rows[0].name).toEqual(name)
expect(response.body.rows[0].description).toEqual(description)
expect(typeof response.body.rows[0].publicInvitation).toBe('string')
})
})

View File

@ -1,62 +0,0 @@
import { Request, Response, Router } from 'express'
import Sequelize from 'sequelize'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import Guild from '../../../../models/Guild'
import Invitation from '../../../../models/Invitation'
import { ObjectAny } from '../../../../typings/utils'
import { paginateModel } from '../../../../tools/database/paginateModel'
export const getPublicDiscoverGuildsRouter = Router()
getPublicDiscoverGuildsRouter.get(
'/guilds/public/discover',
authenticateUser,
async (req: Request, res: Response) => {
const { itemsPerPage, page, search } = req.query as {
itemsPerPage: string
page: string
search?: string
}
const searchLowerCase = search?.toLowerCase()
const result = await paginateModel({
Model: Guild,
queryOptions: { itemsPerPage, page },
findOptions: {
order: [['createdAt', 'DESC']],
include: [Invitation],
where: {
isPublic: true,
...(searchLowerCase != null && {
[Sequelize.Op.or]: [
{
name: Sequelize.where(
Sequelize.fn('LOWER', Sequelize.col('name')),
'LIKE',
`%${searchLowerCase}%`
)
}
]
})
}
}
})
return res.status(200).json({
hasMore: result.hasMore,
totalItems: result.totalItems,
itemsPerPage: result.itemsPerPage,
page: result.page,
rows: result.rows.map((row) => {
const publicInvitation = row.invitations.find(
(invitation) => invitation.isPublic
)
const attributes = row.toJSON() as ObjectAny
delete attributes.invitations
return {
...attributes,
publicInvitation: publicInvitation?.value
}
})
})
}
)

View File

@ -1,21 +1,11 @@
import { Router } from 'express'
import { FastifyPluginAsync } from 'fastify'
import { documentationRouter } from './docs'
import { channelsRouter } from './channels'
import { guildsRouter } from './guilds'
import { invitationsRouter } from './invitations'
import { membersRouter } from './members'
import { messagesRouter } from './messages'
import { uploadsRouter } from './uploads'
import { usersRouter } from './users'
import { usersService } from './users/index.js'
import { guildsService } from './guilds/index.js'
import { uploadsService } from './uploads/index.js'
export const router = Router()
router.use(documentationRouter)
router.use(uploadsRouter)
router.use(usersRouter)
router.use(guildsRouter)
router.use(channelsRouter)
router.use(invitationsRouter)
router.use(messagesRouter)
router.use(membersRouter)
export const services: FastifyPluginAsync = async (fastify) => {
await fastify.register(usersService)
await fastify.register(guildsService)
await fastify.register(uploadsService)
}

View File

@ -1,25 +0,0 @@
/invitations/{invitationId}:
delete:
security:
- bearerAuth: []
tags:
- 'invitations'
summary: 'DELETE an invitation with its id'
parameters:
- name: 'invitationId'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
deletedInvitationId:
type: 'number'

View File

@ -1,40 +0,0 @@
/invitations/{invitationId}:
put:
security:
- bearerAuth: []
tags:
- 'invitations'
summary: 'Update an invitation with its id'
parameters:
- name: 'invitationId'
in: 'path'
required: true
requestBody:
content:
application/json:
schema:
type: 'object'
properties:
value:
type: 'string'
minLength: 1
maxLength: 250
expiresIn:
type: 'integer'
isPublic:
type: 'boolean'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
invitation:
allOf:
- $ref: '#/definitions/Invitation'

View File

@ -1,48 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import { createInvitation } from '../../__test__/utils/createInvitation'
import Invitation from '../../../../models/Invitation'
describe('DELETE /invitations/:invitationId', () => {
it('succeeds and delete the invitation', async () => {
const result = await createInvitation()
const response = await request(application)
.delete(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${result?.user.type as string} ${result?.user.accessToken as string}`)
.send()
.expect(200)
expect(response.body.deletedInvitationId).toEqual(result?.invitation.id)
const foundInvitation = await Invitation.findOne({
where: { id: result?.invitation.id }
})
expect(foundInvitation).toBeNull()
})
it("fails if the invitation doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.delete('/invitations/23')
.set('Authorization', `${userToken.type as string} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it('fails if the user is not the owner', async () => {
const userToken = await authenticateUserTest()
const result = await createInvitation()
const response = await request(application)
.delete(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${userToken.type as string} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,93 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
import { createInvitation } from '../../__test__/utils/createInvitation'
import { errorsMessages } from '../put'
describe('PUT /invitations/:invitationId', () => {
it('succeeds and edit the invitation', async () => {
let value = 'random'
let expiresIn = 0
let isPublic = false
const result = await createInvitation({ value, expiresIn, isPublic })
expect(result?.invitation.value).toEqual(value)
expect(result?.invitation.expiresIn).toEqual(expiresIn)
expect(result?.invitation.isPublic).toEqual(isPublic)
value = 'awesome'
expiresIn = 60
isPublic = true
const response = await request(application)
.put(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${result?.user.type as string} ${result?.user.accessToken as string}`)
.send({ value, expiresIn, isPublic })
.expect(200)
expect(response.body.invitation.value).toEqual(value)
expect(response.body.invitation.isPublic).toEqual(isPublic)
})
it('fails with invalid slug value', async () => {
const result = await createInvitation()
const response = await request(application)
.put(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${result?.user.type as string} ${result?.user.accessToken as string}`)
.send({ value: 'random invitation value' })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining([errorsMessages.value.mustBeSlug]))
})
it('fails with negative expiresIn', async () => {
const result = await createInvitation()
const response = await request(application)
.put(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${result?.user.type as string} ${result?.user.accessToken as string}`)
.send({ expiresIn: -42 })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining([errorsMessages.expiresIn.mustBeGreaterOrEqual]))
})
it("fails if the invitation doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.put('/invitations/23')
.set('Authorization', `${userToken.type as string} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it('fails if the invitation slug value already exists', async () => {
const value = 'random'
const result = await createInvitation({ value })
const response = await request(application)
.put(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${result?.user.type as string} ${result?.user.accessToken as string}`)
.send({ value })
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining(['Value already used'])
)
})
it('fails if the user is not the owner', async () => {
const userToken = await authenticateUserTest()
const result = await createInvitation()
const response = await request(application)
.put(`/invitations/${result?.invitation.id as number}`)
.set('Authorization', `${userToken.type as string} ${userToken.accessToken}`)
.send({ value: 'somevalue' })
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,40 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import Guild from '../../../models/Guild'
import Invitation from '../../../models/Invitation'
import Member from '../../../models/Member'
import { BadRequestError } from '../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
export const deleteByIdInvitationsRouter = Router()
deleteByIdInvitationsRouter.delete(
'/invitations/:invitationId',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { invitationId } = req.params as { invitationId: string }
const invitation = await Invitation.findOne({ where: { id: invitationId } })
if (invitation == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: invitation.guildId, isOwner: true },
include: [Guild]
})
if (member == null) {
throw new NotFoundError()
}
if (member.guild.isPublic && invitation.isPublic) {
throw new BadRequestError("You can't delete the public invitation")
}
const deletedInvitationId = invitation.id
await invitation.destroy()
return res.status(200).json({ deletedInvitationId })
}
)

View File

@ -1,9 +0,0 @@
import { Router } from 'express'
import { deleteByIdInvitationsRouter } from './delete'
import { putInvitationsRouter } from './put'
export const invitationsGetByIdRouter = Router()
invitationsGetByIdRouter.use('/', deleteByIdInvitationsRouter)
invitationsGetByIdRouter.use('/', putInvitationsRouter)

View File

@ -1,97 +0,0 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../tools/middlewares/validateRequest'
import Invitation from '../../../models/Invitation'
import Member from '../../../models/Member'
import { commonErrorsMessages } from '../../../tools/configurations/constants'
import { alreadyUsedValidation } from '../../../tools/validations/alreadyUsedValidation'
import { BadRequestError } from '../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import Guild from '../../../models/Guild'
export const errorsMessages = {
value: {
mustBeSlug: 'Value must be a slug'
},
expiresIn: {
mustBeGreaterOrEqual: 'ExpiresIn must be >= 0'
},
public: {
alreadyHasInvitation: 'There is already a public invitation for this guild'
}
}
export const putInvitationsRouter = Router()
putInvitationsRouter.put(
'/invitations/:invitationId',
authenticateUser,
[
body('value')
.optional({ nullable: true })
.trim()
.escape()
.isLength({ max: 250, min: 1 })
.withMessage(
commonErrorsMessages.charactersLength('value', { max: 250, min: 1 })
)
.isSlug()
.withMessage(errorsMessages.value.mustBeSlug)
.custom(async (value: string) => {
return await alreadyUsedValidation(Invitation, 'value', value)
}),
body('expiresIn')
.optional({ nullable: true })
.isInt({ min: 0 })
.withMessage(errorsMessages.expiresIn.mustBeGreaterOrEqual),
body('isPublic').optional({ nullable: true }).isBoolean()
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { value, expiresIn, isPublic } = req.body as {
value?: string
expiresIn?: number
isPublic?: boolean
}
const { invitationId } = req.params as { invitationId: string }
const invitation = await Invitation.findOne({ where: { id: invitationId } })
if (invitation == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: invitation.guildId, isOwner: true },
include: [Guild]
})
if (member == null) {
throw new NotFoundError()
}
if (invitation.isPublic && expiresIn != null) {
throw new BadRequestError('You can\'t edit "expiresIn" if it is a public invitation')
}
if (member.guild.isPublic && isPublic != null && !isPublic) {
throw new BadRequestError('You can\'t edit "isPublic" if the guild is still public')
}
let expiresInValue = expiresIn ?? invitation.expiresIn
if (expiresInValue > 0 && expiresIn != null) {
expiresInValue += Date.now()
}
invitation.value = value ?? invitation.value
invitation.expiresIn = expiresInValue
invitation.isPublic = isPublic != null ? isPublic : invitation.isPublic
const foundInvitation = await Invitation.findOne({
where: { isPublic: true, guildId: member.guildId }
})
if (isPublic != null && isPublic && foundInvitation != null) {
throw new BadRequestError(errorsMessages.public.alreadyHasInvitation)
}
await invitation.save()
return res.status(200).json({ invitation })
}
)

View File

@ -1,22 +0,0 @@
definitions:
Invitation:
type: 'object'
properties:
id:
type: 'integer'
description: 'Unique id'
value:
type: 'string'
expiresIn:
type: 'integer'
description: 'expiresIn is how long, in milliseconds, until the invitation expires. Note: 0 = never expires'
isPublic:
type: 'boolean'
guildId:
type: 'integer'
createdAt:
type: 'string'
format: 'date-time'
updatedAt:
type: 'string'
format: 'date-time'

View File

@ -1,74 +0,0 @@
import request from 'supertest'
import application from '../../../../application'
import Guild from '../../../../models/Guild'
import Invitation from '../../../../models/Invitation'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import {
createGuild,
CreateGuildResult
} from '../../../guilds/__test__/utils/createGuild'
interface InvitationOptions {
value?: string
/** expiresIn is how long, in milliseconds, until the invitation expires. Note: 0 = never expires */
expiresIn?: number
isPublic?: boolean
guildName?: string
guildId?: number
}
interface CreateInvitationResult extends CreateGuildResult {
invitation: Invitation
}
export const createInvitation = async (
invitation: InvitationOptions = {}
): Promise<CreateInvitationResult | null> => {
let {
value = 'awesome',
expiresIn = 0,
isPublic = false,
guildName = 'guild',
guildId
} = invitation
const user = {
email: 'test@test.com',
name: 'Test'
}
let result: CreateGuildResult | null = null
if (guildId == null) {
result = await createGuild({
guild: { description: 'description', name: guildName },
user
})
guildId = result.guild.id
} else {
const userToken = await authenticateUserTest({
email: user.email,
name: user.name,
alreadySignedUp: true
})
const guild = (await Guild.findOne({ where: { id: guildId } })) as Guild
result = {
user: {
accessToken: userToken.accessToken,
type: userToken.type,
id: userToken.userId
},
guild
}
}
if (result != null) {
const response = await request(application)
.post(`/guilds/${guildId as number}/invitations`)
.set('Authorization', `${result?.user.type} ${result?.user.accessToken}`)
.send({ value, expiresIn, isPublic })
.expect(201)
return {
...result,
invitation: response.body.invitation
}
}
return null
}

View File

@ -1,9 +0,0 @@
import { Router } from 'express'
import { invitationsGetByIdRouter } from './[invitationId]'
import { invitationsJoinByValueRouter } from './join/[value]'
export const invitationsRouter = Router()
invitationsRouter.use('/', invitationsGetByIdRouter)
invitationsRouter.use('/', invitationsJoinByValueRouter)

View File

@ -1,22 +0,0 @@
/invitations/join/{value}:
get:
security:
- bearerAuth: []
tags:
- 'members'
summary: 'Join a guild with its invitation value (create a member)'
parameters:
- name: 'value'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- '201':
content:
application/json:
schema:
type: 'object'

View File

@ -1,50 +0,0 @@
import request from 'supertest'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../../__test__/utils/formatErrors'
import application from '../../../../../application'
import { createInvitation } from '../../../__test__/utils/createInvitation'
import Member from '../../../../../models/Member'
import { wait } from '../../../../../__test__/utils/wait'
describe('GET /invitations/join/:value', () => {
it('succeeds and create a new member', async () => {
const userToken = await authenticateUserTest()
const result = await createInvitation()
await request(application)
.get(`/invitations/join/${result?.invitation.value as string}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(201)
const foundMember = await Member.findOne({
where: { userId: userToken.userId, guildId: result?.guild.id as number }
})
expect(foundMember).not.toBeNull()
})
it("fails if the invitation doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.get('/invitations/join/somevalue')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
it('fails if the invitation expired', async () => {
const userToken = await authenticateUserTest()
const result = await createInvitation({ expiresIn: 100 })
await wait(200)
const response = await request(application)
.get(`/invitations/join/${result?.invitation.value as string}`)
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['The invitation expired']))
})
})

View File

@ -1,62 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../../tools/middlewares/authenticateUser'
import Invitation from '../../../../models/Invitation'
import Member from '../../../../models/Member'
import { BadRequestError } from '../../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../../tools/errors/NotFoundError'
import { emitToMembers } from '../../../../tools/socket/emitEvents'
import Channel from '../../../../models/Channel'
export const errorsMessages = {
invitationExpired: 'The invitation expired',
alreadyInGuild: 'You are already in the guild'
}
export const joinInvitationsRouter = Router()
joinInvitationsRouter.get(
'/invitations/join/:value',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { value } = req.params as { value: string }
const invitation = await Invitation.findOne({ where: { value } })
if (invitation == null) {
throw new NotFoundError()
}
const isValidInvitation =
invitation.expiresIn === 0 || invitation.expiresIn > Date.now()
if (!isValidInvitation) {
throw new BadRequestError(errorsMessages.invitationExpired)
}
const member = await Member.findOne({
where: { userId: user.id, guildId: invitation.guildId }
})
if (member != null) {
throw new BadRequestError(errorsMessages.alreadyInGuild)
}
const defaultChannel = await Channel.findOne({
where: { guildId: invitation.guildId, isDefault: true }
})
if (defaultChannel == null) {
throw new BadRequestError('The guild should have a default channel.')
}
const createdMember = await Member.create({
userId: user.id,
guildId: invitation.guildId,
isOwner: false,
lastVisitedChannelId: defaultChannel.id
})
await emitToMembers({
event: 'members',
guildId: invitation.guildId,
payload: { action: 'create', item: createdMember }
})
return res.status(201).json({ member: createdMember })
}
)

View File

@ -1,7 +0,0 @@
import { Router } from 'express'
import { joinInvitationsRouter } from './get'
export const invitationsJoinByValueRouter = Router()
invitationsJoinByValueRouter.use('/', joinInvitationsRouter)

View File

@ -1,21 +0,0 @@
definitions:
Member:
type: 'object'
properties:
id:
type: 'integer'
description: 'Unique id'
isOwner:
type: 'boolean'
lastVisitedChannelId:
type: 'integer'
userId:
type: 'integer'
guildId:
type: 'integer'
createdAt:
type: 'string'
format: 'date-time'
updatedAt:
type: 'string'
format: 'date-time'

View File

@ -1,20 +0,0 @@
/members:
delete:
security:
- bearerAuth: []
tags:
- 'members'
summary: 'DELETE a member with its id (leave a guild)'
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
deletedMemberId:
type: 'number'

View File

@ -1,30 +0,0 @@
import request from 'supertest'
import application from '../../../application'
import Member from '../../../models/Member'
import { authenticateUserTest } from '../../../__test__/utils/authenticateUser'
import { createGuild } from '../../guilds/__test__/utils/createGuild'
describe('DELETE /members', () => {
it('succeeds and delete the member', async () => {
const result = await createGuild({
guild: { description: 'testing', name: 'guild' },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const userToken = await authenticateUserTest()
const memberToDelete = await Member.create({
userId: userToken.userId,
guildId: result.guild.id,
lastVisitedChannelId: 1
})
const response = await request(application)
.delete('/members')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(200)
expect(response.body.deletedMemberId).toEqual(memberToDelete.id)
})
})

View File

@ -1,32 +0,0 @@
import { Request, Response, Router } from 'express'
import Member from '../../models/Member'
import { ForbiddenError } from '../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../tools/errors/NotFoundError'
import { authenticateUser } from '../../tools/middlewares/authenticateUser'
import { emitToMembers } from '../../tools/socket/emitEvents'
export const deleteByIdMembersRouter = Router()
deleteByIdMembersRouter.delete(
'/members',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const member = await Member.findOne({ where: { userId: user.id, isOwner: false } })
if (member == null) {
throw new NotFoundError()
}
const deletedMemberId = member.id
await member.destroy()
await emitToMembers({
event: 'members',
guildId: member.guildId,
payload: { action: 'delete', item: member }
})
return res.status(200).json({ deletedMemberId })
}
)

View File

@ -1,7 +0,0 @@
import { Router } from 'express'
import { deleteByIdMembersRouter } from './delete'
export const membersRouter = Router()
membersRouter.use('/', deleteByIdMembersRouter)

View File

@ -1,24 +0,0 @@
/messages/{messageId}:
delete:
security:
- bearerAuth: []
tags:
- 'messages'
summary: 'DELETE a message with its id'
parameters:
- name: 'messageId'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
deletedMessageId:
type: 'number'

View File

@ -1,37 +0,0 @@
/messages/{messageId}:
put:
security:
- bearerAuth: []
tags:
- 'messages'
summary: 'Update a message with its id'
parameters:
- name: 'messageId'
in: 'path'
required: true
requestBody:
content:
application/json:
schema:
type: 'object'
properties:
value:
type: 'string'
minLength: 1
maxLength: 50_000
responses:
allOf:
- $ref: '#/definitions/UnauthorizedError'
- $ref: '#/definitions/ForbiddenError'
- $ref: '#/definitions/BadRequestError'
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
type: 'object'
properties:
message:
allOf:
- $ref: '#/definitions/Message'
- $ref: '#/definitions/User'

View File

@ -1,36 +0,0 @@
import request from 'supertest'
import application from '../../../../application'
import { createMessages } from '../../__test__/utils/createMessages'
import Message from '../../../../models/Message'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
describe('DELETE /messages/:messageId', () => {
it('succeeds and delete the message', async () => {
const result = await createMessages(['awesome'])
const messageToDelete = result.messages[0]
const response = await request(application)
.delete(`/messages/${messageToDelete.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
expect(response.body.deletedMessageId).toEqual(messageToDelete.id)
const foundMessage = await Message.findOne({
where: { id: messageToDelete.id }
})
expect(foundMessage).toBeNull()
})
it("fails if the message doesn't exist", async () => {
const userToken = await authenticateUserTest()
const response = await request(application)
.delete('/messages/23')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send()
.expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
})
})

View File

@ -1,38 +0,0 @@
import request from 'supertest'
import application from '../../../../application'
import { commonErrorsMessages } from '../../../../tools/configurations/constants'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import { createMessages } from '../../__test__/utils/createMessages'
describe('PUT /messages/:messageId', () => {
it('succeeds and edit the message', async () => {
const value = 'awesome message'
const result = await createMessages(['awesome'])
const messageToEdit = result.messages[0]
const response = await request(application)
.put(`/messages/${messageToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value })
.expect(200)
expect(response.body.message).not.toBeNull()
expect(response.body.message.value).toEqual(value)
})
it('fails with no message value', async () => {
const result = await createMessages(['awesome'])
const messageToEdit = result.messages[0]
const response = await request(application)
.put(`/messages/${messageToEdit.id as number}`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(400)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(
expect.arrayContaining([
commonErrorsMessages.charactersLength('value', { min: 1, max: 50_000 })
])
)
})
})

View File

@ -1,61 +0,0 @@
import { Request, Response, Router } from 'express'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import Channel from '../../../models/Channel'
import Member from '../../../models/Member'
import Message from '../../../models/Message'
import { messagesFilePath } from '../../../tools/configurations/constants'
import { BadRequestError } from '../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import { deleteFile } from '../../../tools/utils/deleteFiles'
import { emitToMembers } from '../../../tools/socket/emitEvents'
export const deleteByIdMessagesRouter = Router()
deleteByIdMessagesRouter.delete(
'/messages/:messageId',
authenticateUser,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { messageId } = req.params as { messageId: string }
const message = await Message.findOne({ where: { id: messageId } })
if (message == null) {
throw new NotFoundError()
}
const channel = await Channel.findOne({
where: { id: message.channelId }
})
if (channel == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: channel.guildId }
})
if (member == null) {
throw new NotFoundError()
}
if (!member.isOwner && member.id !== message.memberId) {
throw new BadRequestError(
'You can only delete your messages except if you are owner of the guild'
)
}
const deletedMessageId = message.id
if (message.type === 'file') {
await deleteFile({
basePath: messagesFilePath,
valueSavedInDatabase: message.value
})
}
await message.destroy()
await emitToMembers({
event: 'messages',
guildId: channel.guildId,
payload: { action: 'delete', item: message }
})
return res.status(200).json({ deletedMessageId })
}
)

View File

@ -1,71 +0,0 @@
import { Request, Response, Router } from 'express'
import { body } from 'express-validator'
import { authenticateUser } from '../../../tools/middlewares/authenticateUser'
import { validateRequest } from '../../../tools/middlewares/validateRequest'
import Channel from '../../../models/Channel'
import Member from '../../../models/Member'
import Message from '../../../models/Message'
import { commonErrorsMessages } from '../../../tools/configurations/constants'
import { BadRequestError } from '../../../tools/errors/BadRequestError'
import { ForbiddenError } from '../../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import { emitToMembers } from '../../../tools/socket/emitEvents'
export const putByIdMessagesRouter = Router()
putByIdMessagesRouter.put(
'/messages/:messageId',
authenticateUser,
[
body('value')
.trim()
.escape()
.isLength({ min: 1, max: 50_000 })
.withMessage(
commonErrorsMessages.charactersLength('value', { min: 1, max: 50_000 })
)
],
validateRequest,
async (req: Request, res: Response) => {
if (req.user == null) {
throw new ForbiddenError()
}
const user = req.user.current
const { value } = req.body as { value: string }
const { messageId } = req.params as { messageId: string }
const messageToEdit = await Message.findOne({ where: { id: messageId } })
if (messageToEdit == null) {
throw new NotFoundError()
}
if (messageToEdit.type === 'file') {
throw new BadRequestError(
'You can\'t edit a message with the type "file"'
)
}
const channel = await Channel.findOne({
where: { id: messageToEdit.channelId }
})
if (channel == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: channel.guildId }
})
if (member == null) {
throw new NotFoundError()
}
if (member.id !== messageToEdit.memberId) {
throw new BadRequestError('You can only update your messages')
}
messageToEdit.value = value ?? messageToEdit.value
await messageToEdit.save()
const message = { ...messageToEdit.toJSON(), user: req.user.current }
await emitToMembers({
event: 'messages',
guildId: channel.guildId,
payload: { action: 'update', item: message }
})
return res.status(200).json({ message })
}
)

View File

@ -1,29 +0,0 @@
definitions:
MessageType:
type: 'string'
enum: ['text', 'file']
Message:
type: 'object'
properties:
id:
type: 'integer'
description: 'Unique id'
value:
type: 'string'
type:
allOf:
- $ref: '#/definitions/MessageType'
mimetype:
type: 'string'
example: 'text/plain'
memberId:
type: 'integer'
channelId:
type: 'integer'
createdAt:
type: 'string'
format: 'date-time'
updatedAt:
type: 'string'
format: 'date-time'

View File

@ -1,35 +0,0 @@
import request from 'supertest'
import application from '../../../../application'
import Channel from '../../../../models/Channel'
import Message from '../../../../models/Message'
import { createChannels } from '../../../channels/__test__/utils/createChannel'
import { CreateGuildResult } from '../../../guilds/__test__/utils/createGuild'
interface CreateMessagesResult extends CreateGuildResult {
channels: Channel[]
channelId: number
messages: Message[]
}
export const createMessages = async (
messages: string[]
): Promise<CreateMessagesResult> => {
const channel1 = { name: 'general1', description: 'testing' }
const result = await createChannels([channel1])
const messagesResponses: Message[] = []
const channelId = result.channels[0].id as number
for (const message of messages) {
const response = await request(application)
.post(`/channels/${channelId}/messages`)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send({ value: message, type: 'text' })
.expect(201)
messagesResponses.push(response.body.message)
}
return {
...result,
channelId,
messages: messagesResponses
}
}

View File

@ -1,9 +0,0 @@
import { Router } from 'express'
import { deleteByIdMessagesRouter } from './[messageId]/delete'
import { putByIdMessagesRouter } from './[messageId]/put'
export const messagesRouter = Router()
messagesRouter.use('/', deleteByIdMessagesRouter)
messagesRouter.use('/', putByIdMessagesRouter)

View File

@ -1,61 +0,0 @@
import request from 'supertest'
import fsMock from 'mock-fs'
import application from '../../../application'
import Message from '../../../models/Message'
import { messagesFilePath } from '../../../tools/configurations/constants'
import { createGuild } from '../../guilds/__test__/utils/createGuild'
describe('GET /uploads', () => {
it('succeeds and get the file', async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const messageFileURL = `${messagesFilePath.name}/logo.png`
await Message.create({
value: messageFileURL,
type: 'file',
memberId: 1,
channelId: 1
})
fsMock({
[messagesFilePath.filePath]: {
'logo.png': ''
}
})
await request(application)
.get(messageFileURL)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(200)
})
it("fails if the message doesn't exist", async () => {
const name = 'guild'
const description = 'testing'
const result = await createGuild({
guild: { description, name },
user: {
email: 'test@test.com',
name: 'Test'
}
})
const messageFileURL = `${messagesFilePath.name}/logo.png`
fsMock({
[messagesFilePath.filePath]: {
'logo.png': ''
}
})
await request(application)
.get(messageFileURL)
.set('Authorization', `${result.user.type} ${result.user.accessToken}`)
.send()
.expect(404)
})
})

View File

@ -1,50 +1,27 @@
import {
Request,
Response,
NextFunction,
Router,
static as staticExpress
} from 'express'
import path from 'node:path'
import {
guildsIconPath,
messagesFilePath,
usersLogoPath
} from '../../tools/configurations/constants'
import { ForbiddenError } from '../../tools/errors/ForbiddenError'
import { NotFoundError } from '../../tools/errors/NotFoundError'
import { authenticateUser } from '../../tools/middlewares/authenticateUser'
import Channel from '../../models/Channel'
import Member from '../../models/Member'
import Message from '../../models/Message'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox'
export const uploadsRouter = Router()
const parametersUploadsSchema = Type.Object({
image: Type.String()
})
uploadsRouter.use(guildsIconPath.name, staticExpress(guildsIconPath.filePath))
uploadsRouter.use(usersLogoPath.name, staticExpress(usersLogoPath.filePath))
uploadsRouter.use(
messagesFilePath.name,
authenticateUser,
async (req: Request, _res: Response, next: NextFunction) => {
if (req.user == null) {
throw new ForbiddenError()
type ParametersUploadsSchemaType = Static<typeof parametersUploadsSchema>
const getUploadsSchema: FastifySchema = {
tags: ['uploads'] as string[],
params: parametersUploadsSchema
} as const
export const uploadsService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/users/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('users', image))
}
const user = req.user.current
const messageValue = messagesFilePath.name + req.path
const message = await Message.findOne({
where: { type: 'file', value: messageValue },
include: [Channel]
})
if (message == null) {
throw new NotFoundError()
}
const member = await Member.findOne({
where: { userId: user.id, guildId: message.channel.guildId }
})
if (member == null) {
throw new NotFoundError()
}
return next()
},
staticExpress(messagesFilePath.filePath)
)
})
}

View File

@ -1,18 +0,0 @@
/users/{userId}:
get:
tags:
- 'users'
summary: 'GET the user information with its id'
parameters:
- name: 'userId'
in: 'path'
required: true
responses:
allOf:
- $ref: '#/definitions/NotFoundError'
- '200':
content:
application/json:
schema:
allOf:
- $ref: '#/definitions/User'

View File

@ -1,41 +1,20 @@
import request from 'supertest'
import { application } from '../../../../application.js'
import { userExample } from '../../../../models/User.js'
import { userSettingsExample } from '../../../../models/UserSettings.js'
import { prismaMock } from '../../../../__test__/setup.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import { formatErrors } from '../../../../__test__/utils/formatErrors'
import application from '../../../../application'
describe('GET /users/:userId', () => {
it('should returns the user without the email', async () => {
const { userId } = await authenticateUserTest()
const response = await request(application)
.get(`/users/${userId}`)
.send()
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.user.email).toBeUndefined()
expect(response.body.user.id).toEqual(userId)
})
it('should returns the user with the email', async () => {
const userToken = await authenticateUserTest()
await request(application)
.put('/users/current/settings')
.set('Authorization', `${userToken.type} ${userToken.accessToken}`)
.send({ isPublicEmail: true })
.expect(200)
const response = await request(application)
.get(`/users/${userToken.userId}`)
.send()
.expect(200)
expect(response.body.user).not.toBeNull()
expect(response.body.user.email).not.toBeNull()
expect(response.body.user.id).toEqual(userToken.userId)
})
it("should returns 404 error if the user doesn't exist", async () => {
const response = await request(application).get('/users/1').send().expect(404)
const errors = formatErrors(response.body.errors)
expect(errors.length).toEqual(1)
expect(errors).toEqual(expect.arrayContaining(['Not Found']))
describe('GET /users/[userId]', () => {
it('succeeds', async () => {
prismaMock.guild.findMany.mockResolvedValue([])
prismaMock.user.findUnique.mockResolvedValue(userExample)
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
const response = await application.inject({
method: 'GET',
url: `/users/${userExample.id}`
})
const responseJson = response.json()
expect(response.statusCode).toEqual(200)
expect(responseJson.user.id).toEqual(userExample.id)
expect(responseJson.user.name).toEqual(userExample.name)
})
})

View File

@ -1,30 +1,84 @@
import { Request, Response, Router } from 'express'
import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import User from '../../../models/User'
import UserSetting from '../../../models/UserSetting'
import { NotFoundError } from '../../../tools/errors/NotFoundError'
import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from '../../../models/utils.js'
import { userPublicSchema } from '../../../models/User.js'
import { guildSchema } from '../../../models/Guild.js'
export const getUsersRouter = Router()
const parametersGetUserSchema = Type.Object({
userId: userPublicSchema.id
})
getUsersRouter.get(
'/users/:userId',
[],
async (req: Request, res: Response) => {
const { userId } = req.params as { userId: string }
const user = await User.findOne({ where: { id: userId } })
if (user == null) {
throw new NotFoundError()
}
const userSettings = await UserSetting.findOne({
where: { userId: user.id }
})
if (userSettings == null) {
throw new NotFoundError()
}
const result = Object.assign({}, user.toJSON())
if (!userSettings.isPublicEmail) {
delete result.email
}
return res.status(200).json({ user: result })
export type ParametersGetUser = Static<typeof parametersGetUserSchema>
const getServiceSchema: FastifySchema = {
description: 'GET the public user informations with its id',
tags: ['users'] as string[],
params: parametersGetUserSchema,
response: {
200: Type.Object({
user: Type.Object(userPublicSchema),
guilds: Type.Union([Type.Array(Type.Object(guildSchema)), Type.Null()])
}),
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
)
} as const
export const getUserById: FastifyPluginAsync = async (fastify) => {
fastify.route<{
Params: ParametersGetUser
}>({
method: 'GET',
url: '/users/:userId',
schema: getServiceSchema,
handler: async (request, reply) => {
const { userId } = request.params
const settings = await prisma.userSetting.findFirst({
where: { userId }
})
if (settings == null) {
throw fastify.httpErrors.notFound('User not found')
}
const user = await prisma.user.findUnique({
where: {
id: userId
},
select: {
id: true,
name: true,
email: settings.isPublicEmail,
logo: true,
status: true,
biography: true,
website: true,
createdAt: true,
updatedAt: true
}
})
if (user == null) {
throw fastify.httpErrors.notFound('User not found')
}
reply.statusCode = 200
return {
user: {
...user,
settings
},
guilds: !settings.isPublicGuilds
? null
: await prisma.guild.findMany({
where: {
members: {
some: {
userId
}
}
}
})
}
}
})
}

View File

@ -1,7 +0,0 @@
import { Router } from 'express'
import { getUsersRouter } from './get'
export const usersGetByIdRouter = Router()
usersGetByIdRouter.use('/', getUsersRouter)

View File

@ -1,99 +0,0 @@
definitions:
User:
type: 'object'
properties:
user:
type: object
properties:
id:
type: 'integer'
description: 'Unique id'
name:
type: 'string'
description: 'Unique name'
email:
type: 'string'
description: 'Unique email address'
status:
type: 'string'
biography:
type: 'string'
logo:
type: 'string'
isConfirmed:
type: 'boolean'
createdAt:
type: 'string'
format: 'date-time'
updatedAt:
type: 'string'
format: 'date-time'
Language:
type: 'string'
enum: ['en', 'fr']
default: 'en'
Theme:
type: 'string'
enum: ['dark', 'light']
default: 'dark'
UserSettings:
type: 'object'
properties:
language:
allOf:
- $ref: '#/definitions/Language'
theme:
allOf:
- $ref: '#/definitions/Theme'
isPublicEmail:
type: 'boolean'
UserSettingsObject:
type: 'object'
properties:
settings:
allOf:
- $ref: '#/definitions/UserSettings'
AuthenticationStrategy:
type: 'string'
enum: ['local', 'google', 'github', 'discord']
UserStrategies:
type: 'object'
properties:
strategies:
type: 'array'
items:
allOf:
- $ref: '#/definitions/AuthenticationStrategy'
UserCurrentStrategy:
type: 'object'
properties:
currentStrategy:
allOf:
- $ref: '#/definitions/AuthenticationStrategy'
AccessTokenResponse:
type: 'object'
properties:
accessToken:
type: 'string'
expiresIn:
type: 'number'
description: 'expiresIn is how long, in milliseconds, until the returned accessToken expires'
type:
type: 'string'
enum: ['Bearer']
RefreshTokenResponse:
allOf:
- $ref: '#/definitions/AccessTokenResponse'
- type: 'object'
properties:
refreshToken:
type: 'string'

View File

@ -1,89 +0,0 @@
import OAuth, { ProviderOAuth } from '../../../models/OAuth'
import User, { UserRequest } from '../../../models/User'
import UserSetting from '../../../models/UserSetting'
import {
expiresIn,
generateAccessToken,
generateRefreshToken,
ResponseJWT
} from '../../../tools/configurations/jwtToken'
interface ProviderData {
name: string
id: number | string
}
type ResponseCallbackAddStrategy =
| 'success'
| 'This account is already used by someone else'
| 'You are already using this account'
export class OAuthStrategy {
constructor (public provider: ProviderOAuth) {}
async callbackAddStrategy (
providerData: ProviderData,
userRequest: UserRequest
): Promise<ResponseCallbackAddStrategy> {
const OAuthUser = await OAuth.findOne({
where: { providerId: providerData.id, provider: this.provider }
})
let message: ResponseCallbackAddStrategy = 'success'
if (OAuthUser == null) {
await OAuth.create({
provider: this.provider,
providerId: providerData.id,
userId: userRequest.current.id
})
} else if (OAuthUser.userId !== userRequest.current.id) {
message = 'This account is already used by someone else'
} else {
message = 'You are already using this account'
}
return message
}
async callbackSignin (providerData: ProviderData): Promise<ResponseJWT> {
const OAuthUser = await OAuth.findOne({
where: { providerId: providerData.id, provider: this.provider }
})
let userId: number = OAuthUser?.userId ?? 0
if (OAuthUser == null) {
let name = providerData.name
let isAlreadyUsedName = true
let countId: string | number = providerData.id
while (isAlreadyUsedName) {
const foundUsers = await User.count({ where: { name } })
isAlreadyUsedName = foundUsers > 0
if (isAlreadyUsedName) {
name = `${name}-${countId}`
countId = Math.random() * Date.now()
}
}
const user = await User.create({ name })
await UserSetting.create({ userId: user.id })
userId = user.id
await OAuth.create({
provider: this.provider,
providerId: providerData.id,
userId: user.id
})
}
const accessToken = generateAccessToken({
currentStrategy: this.provider,
id: userId
})
const refreshToken = await generateRefreshToken({
currentStrategy: this.provider,
id: userId
})
return {
accessToken,
refreshToken,
expiresIn,
type: 'Bearer'
}
}
}

View File

@ -1,119 +0,0 @@
import { OAuthStrategy } from '../OAuthStrategy'
import OAuth from '../../../../models/OAuth'
import User from '../../../../models/User'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser'
import RefreshToken from '../../../../models/RefreshToken'
const oauthStrategy = new OAuthStrategy('discord')
describe('/users/utils/OAuthStrategy - callbackSignin', () => {
it('should signup the user', async () => {
let users = await User.findAll()
let oauths = await OAuth.findAll()
expect(users.length).toEqual(0)
expect(oauths.length).toEqual(0)
const name = 'Martin'
const id = '12345'
await oauthStrategy.callbackSignin({ id, name })
users = await User.findAll()
oauths = await OAuth.findAll()
expect(users.length).toEqual(1)
expect(oauths.length).toEqual(1)
expect(users[0].name).toEqual(name)
expect(oauths[0].providerId).toEqual(id)
expect(oauths[0].provider).toEqual(oauthStrategy.provider)
})
it('should signup the user and generate a new name when already used', async () => {
const oauths = await OAuth.findAll()
expect(oauths.length).toEqual(0)
const name = 'Martin'
const id = '1234'
await authenticateUserTest({
name,
shouldBeConfirmed: true,
email: 'martin@example.com',
password: 'password'
})
await oauthStrategy.callbackSignin({ id, name })
const oauth = await OAuth.findOne({ where: { providerId: id } })
expect(oauth?.provider).toEqual(oauthStrategy.provider)
expect(oauth?.providerId).toEqual(id)
expect(oauth?.userId).toEqual(2)
const user = await User.findByPk(oauth?.userId)
expect(user?.name.startsWith(name)).toBeTruthy()
expect(user?.name).not.toEqual(name)
})
it('should signin the user if already connected with the provider', async () => {
const name = 'Martin'
const id = '1234'
await oauthStrategy.callbackSignin({ id, name })
let oauths = await OAuth.findAll()
expect(oauths.length).toEqual(1)
await oauthStrategy.callbackSignin({ id, name })
oauths = await OAuth.findAll()
expect(oauths.length).toEqual(1)
})
})
describe('/users/utils/OAuthStrategy - callbackAddStrategy', () => {
it('should add the strategy', async () => {
const userTokens = await authenticateUserTest()
const user = await User.findOne({ where: { id: userTokens.userId } })
expect(user).not.toBeNull()
if (user != null) {
const result = await oauthStrategy.callbackAddStrategy(
{ name: user.name, id: '1234' },
{
current: user,
accessToken: userTokens.accessToken,
currentStrategy: 'local'
}
)
expect(result).toEqual('success')
}
})
it('should not add the strategy if the account of the provider is already used', async () => {
const userTokens = await authenticateUserTest()
const user = await User.findOne({ where: { id: userTokens.userId } })
const name = 'Martin'
const id = '1234'
await oauthStrategy.callbackSignin({ id, name })
expect(user).not.toBeNull()
if (user != null) {
const result = await oauthStrategy.callbackAddStrategy(
{ name: user.name, id: '1234' },
{
current: user,
accessToken: userTokens.accessToken,
currentStrategy: 'local'
}
)
expect(result).toEqual('This account is already used by someone else')
}
})
it('should not add the strategy if the user is already connected with it', async () => {
const name = 'Martin'
const id = '1234'
const userTokens = await oauthStrategy.callbackSignin({ id, name })
const refreshToken = await RefreshToken.findOne({
where: { token: userTokens.refreshToken as string },
include: [{ model: User }]
})
expect(refreshToken).not.toBeNull()
if (refreshToken != null) {
const result = await oauthStrategy.callbackAddStrategy(
{ name: refreshToken.user.name, id: '1234' },
{
current: refreshToken.user,
accessToken: userTokens.accessToken,
currentStrategy: oauthStrategy.provider
}
)
expect(result).toEqual('You are already using this account')
}
})
})

View File

@ -1,20 +0,0 @@
import { buildQueryURL } from '../buildQueryURL'
test('controllers/users/utils/buildQueryUrl', () => {
expect(
buildQueryURL('http://localhost:8080', {
test: 'query'
})
).toEqual('http://localhost:8080/?test=query')
expect(
buildQueryURL('http://localhost:8080/', {
test: 'query'
})
).toEqual('http://localhost:8080/?test=query')
expect(
buildQueryURL('http://localhost:3000', {
test: 'query',
code: 'abc'
})
).toEqual('http://localhost:3000/?test=query&code=abc')
})

View File

@ -1,12 +0,0 @@
import { ObjectAny } from '../../../typings/utils'
export const buildQueryURL = (
baseURL: string,
queryObject: ObjectAny
): string => {
const url = new URL(baseURL)
Object.entries(queryObject).forEach(([query, value]) => {
url.searchParams.append(query, value)
})
return url.href
}

Some files were not shown because too many files have changed in this diff Show More