chore: initial commit

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

View File

@ -0,0 +1,24 @@
/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

@ -0,0 +1,25 @@
/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

@ -0,0 +1,48 @@
/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

@ -0,0 +1,62 @@
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

@ -0,0 +1,58 @@
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

@ -0,0 +1,182 @@
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

@ -0,0 +1,31 @@
/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

@ -0,0 +1,39 @@
/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

@ -0,0 +1,23 @@
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

@ -0,0 +1,146 @@
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

@ -0,0 +1,43 @@
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

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

View File

@ -0,0 +1,73 @@
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

@ -0,0 +1,57 @@
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

@ -0,0 +1,29 @@
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

@ -0,0 +1,19 @@
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

@ -0,0 +1,31 @@
/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

@ -0,0 +1,40 @@
/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

@ -0,0 +1,46 @@
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

@ -0,0 +1,163 @@
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

@ -0,0 +1,43 @@
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

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

View File

@ -0,0 +1,89 @@
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

@ -0,0 +1,32 @@
/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

@ -0,0 +1,35 @@
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

@ -0,0 +1,50 @@
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

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

View File

@ -0,0 +1,121 @@
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 })
}
)