feat: migrate from express to fastify
This commit is contained in:
@ -1,17 +0,0 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import application from '../application'
|
||||
import { usersLogoPath } from '../tools/configurations/constants'
|
||||
|
||||
describe('application', () => {
|
||||
it("returns a 404 on route that doesn't exist", async () => {
|
||||
return await request(application).post('/404routenotfound').send().expect(404)
|
||||
})
|
||||
|
||||
it('returns a 200 success code for users images', async () => {
|
||||
return await request(application)
|
||||
.get(`${usersLogoPath.name}/default.png`)
|
||||
.send()
|
||||
.expect(200)
|
||||
})
|
||||
})
|
@ -1,4 +1,3 @@
|
||||
process.env.DATABASE_DIALECT = 'sqlite'
|
||||
process.env.JWT_ACCESS_EXPIRES_IN = '15 minutes'
|
||||
process.env.JWT_ACCESS_SECRET = 'accessTokenSecret'
|
||||
process.env.JWT_REFRESH_SECRET = 'refreshTokenSecret'
|
@ -1,11 +1,8 @@
|
||||
import fsMock from 'mock-fs'
|
||||
import path from 'path'
|
||||
import { Sequelize } from 'sequelize-typescript'
|
||||
import { Database, open } from 'sqlite'
|
||||
import sqlite3 from 'sqlite3'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended'
|
||||
import { DeepMockProxy } from 'jest-mock-extended/lib/cjs/Mock'
|
||||
|
||||
let sqlite: Database | undefined
|
||||
let sequelize: Sequelize | undefined
|
||||
import prisma from '../tools/database/prisma.js'
|
||||
|
||||
jest.mock('nodemailer', () => ({
|
||||
createTransport: () => {
|
||||
@ -15,28 +12,13 @@ jest.mock('nodemailer', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
beforeAll(async () => {
|
||||
sqlite = await open({
|
||||
filename: ':memory:',
|
||||
driver: sqlite3.Database
|
||||
})
|
||||
sequelize = new Sequelize({
|
||||
dialect: process.env.DATABASE_DIALECT,
|
||||
storage: process.env.DATABASE_DIALECT === 'sqlite' ? ':memory:' : undefined,
|
||||
logging: false,
|
||||
models: [path.join(__dirname, '..', 'models')]
|
||||
})
|
||||
jest.mock('../tools/database/prisma.js', () => ({
|
||||
__esModule: true,
|
||||
default: mockDeep<PrismaClient>()
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
mockReset(prismaMock)
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await sequelize?.sync({ force: true })
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
fsMock.restore()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await sqlite?.close()
|
||||
await sequelize?.close()
|
||||
})
|
||||
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { formatErrors } from '../formatErrors'
|
||||
|
||||
test('__test__/utils/formatErrors', () => {
|
||||
expect(formatErrors('randomSring')).toEqual([])
|
||||
const errors = [
|
||||
{ message: 'some error message' },
|
||||
{ message: 'another error' }
|
||||
]
|
||||
expect(formatErrors(errors)).toEqual(['some error message', 'another error'])
|
||||
})
|
@ -1,57 +0,0 @@
|
||||
import request from 'supertest'
|
||||
|
||||
import application from '../../application'
|
||||
import User from '../../models/User'
|
||||
|
||||
interface AuthenticateUserOptions {
|
||||
name?: string
|
||||
email?: string
|
||||
password?: string
|
||||
shouldBeConfirmed?: boolean
|
||||
alreadySignedUp?: boolean
|
||||
}
|
||||
|
||||
export async function authenticateUserTest (
|
||||
options: AuthenticateUserOptions = {}
|
||||
): Promise<{
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiresIn: string
|
||||
type: 'Bearer'
|
||||
userId: number
|
||||
}> {
|
||||
const {
|
||||
name = 'John',
|
||||
email = 'contact@test.com',
|
||||
shouldBeConfirmed = true,
|
||||
password = 'test',
|
||||
alreadySignedUp = false
|
||||
} = options
|
||||
|
||||
if (!alreadySignedUp) {
|
||||
const { body: signupBody } = await request(application)
|
||||
.post('/users/signup')
|
||||
.send({ name, email, password })
|
||||
.expect(201)
|
||||
let signinResponse: any = { body: {} }
|
||||
if (shouldBeConfirmed) {
|
||||
const user = await User.findOne({ where: { id: signupBody.user.id } })
|
||||
await request(application)
|
||||
.get(`/users/confirmEmail?tempToken=${user?.tempToken as string}`)
|
||||
.send()
|
||||
.expect(200)
|
||||
signinResponse = await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password })
|
||||
.expect(200)
|
||||
}
|
||||
|
||||
return { ...signinResponse.body, userId: signupBody.user.id }
|
||||
}
|
||||
const signinResponse = await request(application)
|
||||
.post('/users/signin')
|
||||
.send({ email, password })
|
||||
.expect(200)
|
||||
const user = await User.findOne({ where: { email } })
|
||||
return { ...signinResponse.body, userId: user?.id }
|
||||
}
|
28
src/__test__/utils/authenticateUserTest.ts
Normal file
28
src/__test__/utils/authenticateUserTest.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { User } from '@prisma/client'
|
||||
|
||||
import { refreshTokenExample } from '../../models/RefreshToken.js'
|
||||
import { userExample, UserJWT } from '../../models/User.js'
|
||||
import { userSettingsExample } from '../../models/UserSettings.js'
|
||||
import {
|
||||
generateAccessToken,
|
||||
generateRefreshToken
|
||||
} from '../../tools/utils/jwtToken'
|
||||
import { prismaMock } from '../setup'
|
||||
|
||||
export const authenticateUserTest = async (): Promise<{
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
user: User
|
||||
}> => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(userExample)
|
||||
prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample)
|
||||
prismaMock.oAuth.findMany.mockResolvedValue([])
|
||||
prismaMock.refreshToken.create.mockResolvedValue(refreshTokenExample)
|
||||
const userJWT: UserJWT = {
|
||||
currentStrategy: 'local',
|
||||
id: 1
|
||||
}
|
||||
const accessToken = generateAccessToken(userJWT)
|
||||
const refreshToken = await generateRefreshToken(userJWT)
|
||||
return { accessToken, refreshToken, user: userExample }
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
/** formatErrors for testing purpose (no types safety) */
|
||||
export const formatErrors = (errors: any): string[] => {
|
||||
try {
|
||||
return errors.map((e: any) => e.message)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export const wait = async (ms: number): Promise<void> => {
|
||||
return await new Promise((resolve) => {
|
||||
setTimeout(resolve, ms)
|
||||
})
|
||||
}
|
@ -1,45 +1,51 @@
|
||||
import 'express-async-errors'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import cors from 'cors'
|
||||
import dotenv from 'dotenv'
|
||||
import express, { Request } from 'express'
|
||||
import rateLimit from 'express-rate-limit'
|
||||
import helmet from 'helmet'
|
||||
import morgan from 'morgan'
|
||||
import fastify from 'fastify'
|
||||
import fastifyCors from 'fastify-cors'
|
||||
import fastifySwagger from 'fastify-swagger'
|
||||
import fastifyUrlData from 'fastify-url-data'
|
||||
import fastifyHelmet from 'fastify-helmet'
|
||||
import fastifyRateLimit from 'fastify-rate-limit'
|
||||
import fastifySensible from 'fastify-sensible'
|
||||
import fastifyStatic from 'fastify-static'
|
||||
|
||||
import { errorHandler } from './tools/middlewares/errorHandler'
|
||||
import { router } from './services'
|
||||
import { NotFoundError } from './tools/errors/NotFoundError'
|
||||
import { TooManyRequestsError } from './tools/errors/TooManyRequestsError'
|
||||
import { services } from './services/index.js'
|
||||
import { swaggerOptions } from './tools/configurations/swaggerOptions.js'
|
||||
import fastifySocketIo from './tools/plugins/socket-io.js'
|
||||
import { UPLOADS_URL } from './tools/configurations/index.js'
|
||||
|
||||
const application = express()
|
||||
export const application = fastify({
|
||||
logger: process.env.NODE_ENV === 'development'
|
||||
})
|
||||
dotenv.config()
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
application.use(morgan<Request>('dev'))
|
||||
} else if (process.env.NODE_ENV === 'production') {
|
||||
const requestPerSecond = 2
|
||||
const seconds = 60
|
||||
const windowMs = seconds * 1000
|
||||
application.enable('trust proxy')
|
||||
application.use(
|
||||
rateLimit({
|
||||
windowMs,
|
||||
max: seconds * requestPerSecond,
|
||||
handler: () => {
|
||||
throw new TooManyRequestsError()
|
||||
}
|
||||
})
|
||||
)
|
||||
const main = async (): Promise<void> => {
|
||||
await application.register(fastifyCors)
|
||||
await application.register(fastifySensible)
|
||||
await application.register(fastifyUrlData)
|
||||
await application.register(fastifySocketIo, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
preflightContinue: false,
|
||||
optionsSuccessStatus: 204
|
||||
}
|
||||
})
|
||||
await application.register(fastifyHelmet)
|
||||
await application.register(fastifyRateLimit, {
|
||||
max: 100,
|
||||
timeWindow: '1 minute'
|
||||
})
|
||||
await application.register(fastifyStatic, {
|
||||
root: fileURLToPath(UPLOADS_URL),
|
||||
prefix: '/uploads/'
|
||||
})
|
||||
await application.register(fastifySwagger, swaggerOptions)
|
||||
await application.register(services)
|
||||
}
|
||||
|
||||
application.use(express.json())
|
||||
application.use(helmet())
|
||||
application.use(cors<Request>())
|
||||
application.use(router)
|
||||
application.use(() => {
|
||||
throw new NotFoundError()
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
application.use(errorHandler)
|
||||
|
||||
export default application
|
||||
|
30
src/index.ts
30
src/index.ts
@ -1,22 +1,12 @@
|
||||
import { authorize } from '@thream/socketio-jwt'
|
||||
import { application } from './application.js'
|
||||
import { HOST, PORT } from './tools/configurations/index.js'
|
||||
|
||||
import application from './application'
|
||||
import { socket } from './tools/socket'
|
||||
import { sequelize } from './tools/database/sequelize'
|
||||
const main = async (): Promise<void> => {
|
||||
const address = await application.listen(PORT, HOST)
|
||||
console.log('\x1b[36m%s\x1b[0m', `🚀 Server listening at ${address}`)
|
||||
}
|
||||
|
||||
const PORT = parseInt(process.env.PORT ?? '8080', 10)
|
||||
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
const server = application.listen(PORT, () => {
|
||||
console.log('\x1b[36m%s\x1b[0m', `🚀 Server listening on port ${PORT}.`)
|
||||
})
|
||||
socket.init(server)
|
||||
socket.io?.use(
|
||||
authorize({
|
||||
secret: process.env.JWT_ACCESS_SECRET
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch((error) => console.error(error))
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
@ -1,55 +1,23 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { Channel } from '@prisma/client'
|
||||
|
||||
import Guild from './Guild'
|
||||
import Message from './Message'
|
||||
import { date, id } from './utils.js'
|
||||
import { guildExample } from './Guild.js'
|
||||
|
||||
export const channelTypes = ['text', 'voice'] as const
|
||||
export type ChannelType = typeof channelTypes[number]
|
||||
export const types = [Type.Literal('text')]
|
||||
|
||||
@Table
|
||||
export default class Channel extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false
|
||||
})
|
||||
name!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'text'
|
||||
})
|
||||
type!: ChannelType
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
})
|
||||
description!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
})
|
||||
isDefault!: boolean
|
||||
|
||||
@ForeignKey(() => Guild)
|
||||
@Column
|
||||
guildId!: number
|
||||
|
||||
@BelongsTo(() => Guild)
|
||||
guild!: Guild
|
||||
|
||||
@HasMany(() => Message)
|
||||
messages!: Message[]
|
||||
export const channelSchema = {
|
||||
id,
|
||||
name: Type.String({ maxLength: 255 }),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
guildId: id
|
||||
}
|
||||
|
||||
export const channelExample: Channel = {
|
||||
id: 1,
|
||||
name: 'general',
|
||||
guildId: guildExample.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
@ -1,45 +1,22 @@
|
||||
import { Column, DataType, HasMany, Model, Table } from 'sequelize-typescript'
|
||||
import { guildsIconPath } from '../tools/configurations/constants'
|
||||
import { Guild } from '@prisma/client'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
import Channel from './Channel'
|
||||
import Invitation from './Invitation'
|
||||
import Member from './Member'
|
||||
import { date, id } from './utils.js'
|
||||
|
||||
@Table
|
||||
export default class Guild extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false
|
||||
})
|
||||
name!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
})
|
||||
description!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: `${guildsIconPath.name}/default.png`
|
||||
})
|
||||
icon!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
})
|
||||
isPublic!: boolean
|
||||
|
||||
@HasMany(() => Member, { onDelete: 'CASCADE' })
|
||||
members!: Member[]
|
||||
|
||||
@HasMany(() => Invitation, { onDelete: 'CASCADE' })
|
||||
invitations!: Invitation[]
|
||||
|
||||
@HasMany(() => Channel)
|
||||
channels!: Channel[]
|
||||
export const guildSchema = {
|
||||
id,
|
||||
name: Type.String({ minLength: 3, maxLength: 30 }),
|
||||
icon: Type.String({ format: 'uri-reference' }),
|
||||
description: Type.String({ maxLength: 160 }),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt
|
||||
}
|
||||
|
||||
export const guildExample: Guild = {
|
||||
id: 1,
|
||||
name: 'GuildExample',
|
||||
description: 'guild example.',
|
||||
icon: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import Guild from './Guild'
|
||||
|
||||
@Table
|
||||
export default class Invitation extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false
|
||||
})
|
||||
value!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: false
|
||||
})
|
||||
/** expiresIn is how long, in milliseconds, until the invitation expires. Note: 0 = never expires */
|
||||
expiresIn!: number
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
})
|
||||
isPublic!: boolean
|
||||
|
||||
@ForeignKey(() => Guild)
|
||||
@Column
|
||||
guildId!: number
|
||||
|
||||
@BelongsTo(() => Guild, { onDelete: 'CASCADE' })
|
||||
guild!: Guild
|
||||
}
|
@ -1,48 +1,24 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
HasMany,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { Member } from '@prisma/client'
|
||||
|
||||
import Channel from './Channel'
|
||||
import Guild from './Guild'
|
||||
import Message from './Message'
|
||||
import User from './User'
|
||||
import { date, id } from './utils.js'
|
||||
import { guildExample } from './Guild.js'
|
||||
import { userExample } from './User.js'
|
||||
|
||||
@Table
|
||||
export default class Member extends Model {
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
})
|
||||
isOwner!: boolean
|
||||
|
||||
@ForeignKey(() => Channel)
|
||||
@Column
|
||||
lastVisitedChannelId!: number
|
||||
|
||||
@BelongsTo(() => Channel)
|
||||
channel!: Channel
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId!: number
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user!: User
|
||||
|
||||
@ForeignKey(() => Guild)
|
||||
@Column
|
||||
guildId!: number
|
||||
|
||||
@BelongsTo(() => Guild)
|
||||
guild!: Guild
|
||||
|
||||
@HasMany(() => Message, { onDelete: 'CASCADE' })
|
||||
messages!: Message[]
|
||||
export const memberSchema = {
|
||||
id,
|
||||
isOwner: Type.Boolean({ default: false }),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
userId: id,
|
||||
guildId: id
|
||||
}
|
||||
|
||||
export const memberExample: Member = {
|
||||
id: 1,
|
||||
isOwner: true,
|
||||
userId: userExample.id,
|
||||
guildId: guildExample.id,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
@ -1,51 +1,20 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
import Channel from './Channel'
|
||||
import Member from './Member'
|
||||
import { date, id } from './utils.js'
|
||||
|
||||
export const messageTypes = ['text', 'file'] as const
|
||||
export type MessageType = typeof messageTypes[number]
|
||||
export const types = [Type.Literal('text'), Type.Literal('file')]
|
||||
|
||||
@Table
|
||||
export default class Message extends Model {
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: false
|
||||
})
|
||||
value!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'text'
|
||||
})
|
||||
type!: MessageType
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'text/plain'
|
||||
})
|
||||
mimetype!: string
|
||||
|
||||
@ForeignKey(() => Member)
|
||||
@Column
|
||||
memberId!: number
|
||||
|
||||
@BelongsTo(() => Member)
|
||||
member!: Member
|
||||
|
||||
@ForeignKey(() => Channel)
|
||||
@Column
|
||||
channelId!: number
|
||||
|
||||
@BelongsTo(() => Channel)
|
||||
channel!: Channel
|
||||
export const messageSchema = {
|
||||
id,
|
||||
value: Type.String(),
|
||||
type: Type.Union(types, { default: 'text' }),
|
||||
mimetype: Type.String({
|
||||
maxLength: 255,
|
||||
default: 'text/plain',
|
||||
format: 'mimetype'
|
||||
}),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
memberId: id,
|
||||
channelId: id
|
||||
}
|
||||
|
@ -1,38 +1,25 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
import User from './User'
|
||||
import { date, id } from './utils.js'
|
||||
|
||||
export const providers = ['google', 'github', 'discord'] as const
|
||||
export const strategies = [...providers, 'local'] as const
|
||||
|
||||
export const strategiesTypebox = strategies.map((strategy) =>
|
||||
Type.Literal(strategy)
|
||||
)
|
||||
export const providersTypebox = providers.map((provider) =>
|
||||
Type.Literal(provider)
|
||||
)
|
||||
|
||||
export type ProviderOAuth = typeof providers[number]
|
||||
export type AuthenticationStrategy = typeof strategies[number]
|
||||
|
||||
@Table
|
||||
export default class OAuth extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false
|
||||
})
|
||||
provider!: ProviderOAuth
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: false
|
||||
})
|
||||
providerId!: string
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId!: number
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user!: User
|
||||
export const oauthSchema = {
|
||||
id,
|
||||
providerId: Type.String(),
|
||||
provider: Type.Union([...providersTypebox]),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
userId: id
|
||||
}
|
||||
|
@ -1,26 +1,21 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
import { RefreshToken } from '@prisma/client'
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
import User from './User'
|
||||
import { userExample } from './User.js'
|
||||
import { date, id } from './utils.js'
|
||||
|
||||
@Table
|
||||
export default class RefreshToken extends Model {
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: false
|
||||
})
|
||||
token!: string
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId!: number
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user!: User
|
||||
export const refreshTokensSchema = {
|
||||
id,
|
||||
token: Type.String(),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
userId: id
|
||||
}
|
||||
|
||||
export const refreshTokenExample: RefreshToken = {
|
||||
id: 1,
|
||||
userId: userExample.id,
|
||||
token: 'sometoken',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
@ -1,26 +1,9 @@
|
||||
import {
|
||||
Column,
|
||||
DataType,
|
||||
HasMany,
|
||||
HasOne,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
import { User } from '@prisma/client'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
|
||||
import Member from './Member'
|
||||
import OAuth, { AuthenticationStrategy } from './OAuth'
|
||||
import RefreshToken from './RefreshToken'
|
||||
import UserSetting from './UserSetting'
|
||||
import { deleteObjectAttributes } from '../tools/utils/deleteObjectAttributes'
|
||||
import { usersLogoPath } from '../tools/configurations/constants'
|
||||
|
||||
export const userHiddenAttributes = [
|
||||
'password',
|
||||
'tempToken',
|
||||
'tempExpirationToken'
|
||||
] as const
|
||||
export type UserHiddenAttributes = typeof userHiddenAttributes[number]
|
||||
export interface UserToJSON extends Omit<User, UserHiddenAttributes> {}
|
||||
import { AuthenticationStrategy, strategiesTypebox } from './OAuth.js'
|
||||
import { userSettingsSchema } from './UserSettings.js'
|
||||
import { date, id } from './utils.js'
|
||||
|
||||
export interface UserJWT {
|
||||
id: number
|
||||
@ -33,80 +16,66 @@ export interface UserRequest {
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
@Table
|
||||
export default class User extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false
|
||||
})
|
||||
name!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: true
|
||||
})
|
||||
email?: string
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: true
|
||||
})
|
||||
password?: string
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
})
|
||||
status!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: ''
|
||||
})
|
||||
biography!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: false,
|
||||
defaultValue: `${usersLogoPath.name}/default.png`
|
||||
})
|
||||
logo!: string
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false
|
||||
})
|
||||
isConfirmed!: boolean
|
||||
|
||||
@Column({
|
||||
type: DataType.TEXT,
|
||||
allowNull: true
|
||||
})
|
||||
tempToken?: string | null
|
||||
|
||||
@Column({
|
||||
type: DataType.BIGINT,
|
||||
allowNull: true
|
||||
})
|
||||
tempExpirationToken?: number | null
|
||||
|
||||
@HasMany(() => RefreshToken, { onDelete: 'CASCADE' })
|
||||
refreshTokens!: RefreshToken[]
|
||||
|
||||
@HasMany(() => OAuth, { onDelete: 'CASCADE' })
|
||||
OAuths!: OAuth[]
|
||||
|
||||
@HasMany(() => Member, { onDelete: 'CASCADE' })
|
||||
members!: Member[]
|
||||
|
||||
@HasOne(() => UserSetting, { onDelete: 'CASCADE' })
|
||||
settings!: UserSetting
|
||||
|
||||
toJSON (): UserToJSON {
|
||||
const attributes = Object.assign({}, this.get())
|
||||
return deleteObjectAttributes(attributes, userHiddenAttributes) as UserToJSON
|
||||
}
|
||||
export const userSchema = {
|
||||
id,
|
||||
name: Type.String({ minLength: 1, maxLength: 30 }),
|
||||
email: Type.String({ minLength: 1, maxLength: 255, format: 'email' }),
|
||||
password: Type.String(),
|
||||
logo: Type.String({ format: 'uri-reference' }),
|
||||
status: Type.String({ maxLength: 255 }),
|
||||
biography: Type.String(),
|
||||
website: Type.String({ maxLength: 255, format: 'uri-reference' }),
|
||||
isConfirmed: Type.Boolean({ default: false }),
|
||||
temporaryToken: Type.String(),
|
||||
temporaryExpirationToken: Type.String({ format: 'date-time' }),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt
|
||||
}
|
||||
|
||||
export const userPublicSchema = {
|
||||
id,
|
||||
name: userSchema.name,
|
||||
email: Type.Optional(userSchema.email),
|
||||
logo: Type.Optional(userSchema.logo),
|
||||
status: Type.Optional(userSchema.status),
|
||||
biography: Type.Optional(userSchema.biography),
|
||||
website: Type.Optional(userSchema.website),
|
||||
isConfirmed: userSchema.isConfirmed,
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
settings: Type.Optional(Type.Object(userSettingsSchema))
|
||||
}
|
||||
|
||||
export const userCurrentSchema = Type.Object({
|
||||
user: Type.Object({
|
||||
...userPublicSchema,
|
||||
currentStrategy: Type.Union([...strategiesTypebox]),
|
||||
strategies: Type.Array(Type.Union([...strategiesTypebox]))
|
||||
})
|
||||
})
|
||||
|
||||
export const bodyUserSchema = Type.Object({
|
||||
email: userSchema.email,
|
||||
name: userSchema.name,
|
||||
password: userSchema.password,
|
||||
theme: userSettingsSchema.theme,
|
||||
language: userSettingsSchema.language
|
||||
})
|
||||
|
||||
export type BodyUserSchemaType = Static<typeof bodyUserSchema>
|
||||
|
||||
export const userExample: User = {
|
||||
id: 1,
|
||||
name: 'Divlo',
|
||||
email: 'contact@divlo.fr',
|
||||
password: 'somepassword',
|
||||
logo: null,
|
||||
status: null,
|
||||
biography: null,
|
||||
website: null,
|
||||
isConfirmed: true,
|
||||
temporaryToken: 'temporaryUUIDtoken',
|
||||
temporaryExpirationToken: new Date(),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
@ -1,65 +0,0 @@
|
||||
import {
|
||||
BelongsTo,
|
||||
Column,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
Table
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import User from './User'
|
||||
import { deleteObjectAttributes } from '../tools/utils/deleteObjectAttributes'
|
||||
|
||||
export const userSettingHiddenAttributes = [
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'userId',
|
||||
'id'
|
||||
] as const
|
||||
export type UserSettingHiddenAttributes = typeof userSettingHiddenAttributes[number]
|
||||
export interface UserSettingToJSON
|
||||
extends Omit<UserSetting, UserSettingHiddenAttributes> {}
|
||||
|
||||
export const languages = ['fr', 'en'] as const
|
||||
export type Language = typeof languages[number]
|
||||
|
||||
export const themes = ['light', 'dark'] as const
|
||||
export type Theme = typeof themes[number]
|
||||
|
||||
@Table
|
||||
export default class UserSetting extends Model {
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'en'
|
||||
})
|
||||
language!: Language
|
||||
|
||||
@Column({
|
||||
type: DataType.STRING,
|
||||
allowNull: false,
|
||||
defaultValue: 'dark'
|
||||
})
|
||||
theme!: Theme
|
||||
|
||||
@Column({
|
||||
type: DataType.BOOLEAN,
|
||||
defaultValue: false
|
||||
})
|
||||
isPublicEmail!: boolean
|
||||
|
||||
@ForeignKey(() => User)
|
||||
@Column
|
||||
userId?: number
|
||||
|
||||
@BelongsTo(() => User)
|
||||
user!: User
|
||||
|
||||
toJSON (): UserSettingToJSON {
|
||||
const attributes = Object.assign({}, this.get())
|
||||
return deleteObjectAttributes(
|
||||
attributes,
|
||||
userSettingHiddenAttributes
|
||||
) as UserSettingToJSON
|
||||
}
|
||||
}
|
32
src/models/UserSettings.ts
Normal file
32
src/models/UserSettings.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { UserSetting } from '@prisma/client'
|
||||
import { Type, Static } from '@sinclair/typebox'
|
||||
|
||||
import { date, id } from './utils.js'
|
||||
|
||||
export const languages = [Type.Literal('fr'), Type.Literal('en')]
|
||||
export const themes = [Type.Literal('light'), Type.Literal('dark')]
|
||||
|
||||
export const userSettingsSchema = {
|
||||
id,
|
||||
language: Type.Union(languages, { default: 'en' }),
|
||||
theme: Type.Union(themes, { default: 'dark' }),
|
||||
isPublicEmail: Type.Boolean({ default: false }),
|
||||
isPublicGuilds: Type.Boolean({ default: false }),
|
||||
createdAt: date.createdAt,
|
||||
updatedAt: date.updatedAt,
|
||||
userId: id
|
||||
}
|
||||
|
||||
export type Theme = Static<typeof userSettingsSchema.theme>
|
||||
export type Language = Static<typeof userSettingsSchema.language>
|
||||
|
||||
export const userSettingsExample: UserSetting = {
|
||||
id: 1,
|
||||
theme: 'dark',
|
||||
language: 'en',
|
||||
isPublicEmail: false,
|
||||
isPublicGuilds: false,
|
||||
userId: 1,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
-- All users have the password `test`
|
||||
INSERT INTO `Users` (`id`, `name`, `email`, `password`, `status`, `biography`, `logo`, `isConfirmed`, `tempToken`, `tempExpirationToken`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 'Divlo', 'contact@divlo.fr', '$2a$12$rdXfja1jtd88bgvKs4Pbl.yBBFJZP5Y0TcmqOCPm8Fy3BmQCnJHG2', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:47:36', '2021-03-04 12:48:30'),
|
||||
(2, 'Divlo2', 'divlogaming@gmail.com', '$2a$12$/aIvPyRbp/WUXN1FHwo0w.pBtT1dNls01L8SClpDXbBccjWD33trm', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:47:53', '2021-03-04 12:48:32'),
|
||||
(3, 'John Doe', 'johndoe@gmail.com', '$2a$12$3Qif9pviwoLLtTAQZqir7u4stLNU6E053EvDeso16aqvuahi7w1se', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:48:24', '2021-03-04 12:48:35'),
|
||||
(4, 'User', 'user@example.com', '$2a$12$SdgnEhy22aNQXwBRNDy/XeUNWLvu/MneA1Xfs2dtNhai.m/gP9xNi', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:49:58', '2021-03-04 12:50:04');
|
||||
|
||||
INSERT INTO `UserSettings` (`id`, `language`, `theme`, `isPublicEmail`, `userId`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 'en', 'dark', 0, 1, '2021-03-04 12:47:36', '2021-03-04 12:47:36'),
|
||||
(2, 'fr', 'dark', 0, 2, '2021-03-04 12:47:53', '2021-03-04 12:47:53'),
|
||||
(3, 'en', 'light', 0, 3, '2021-03-04 12:48:24', '2021-03-04 12:48:24'),
|
||||
(4, 'fr', 'light', 0, 4, '2021-03-04 12:49:58', '2021-03-04 12:49:58');
|
||||
|
||||
INSERT INTO `Guilds` (`id`, `name`, `description`, `icon`, `isPublic`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 'Ligue.dev', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:51:27', '2021-03-04 12:51:27'),
|
||||
(2, 'Docstring', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:51:39', '2021-03-04 12:51:39'),
|
||||
(3, 'Read The Docs', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:51:50', '2021-03-04 12:51:50'),
|
||||
(4, 'Les Joies du Code', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:09', '2021-03-04 12:52:09'),
|
||||
(5, 'Firecamp', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:19', '2021-03-04 12:52:19'),
|
||||
(6, 'CodinGame', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:30', '2021-03-04 12:52:30'),
|
||||
(7, 'Leon AI', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:38', '2021-03-04 12:52:38'),
|
||||
(8, 'Academind', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:45', '2021-03-04 12:52:45'),
|
||||
(9, 'StandardJS', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:57', '2021-03-04 12:52:57'),
|
||||
(10, 'Next.js', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:53:08', '2021-03-04 12:53:08'),
|
||||
(11, 'Tailwind CSS', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:54:58', '2021-03-04 12:54:58'),
|
||||
(12, 'Vue Land', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:04', '2021-03-04 12:55:04'),
|
||||
(13, 'Nuxt.js', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:11', '2021-03-04 12:55:11'),
|
||||
(14, 'Reactiflux', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:16', '2021-03-04 12:55:16'),
|
||||
(15, 'Deno', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:25', '2021-03-04 12:55:25'),
|
||||
(16, 'fastify', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:33', '2021-03-04 12:55:33'),
|
||||
(17, 'MandarineTS', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:48', '2021-03-04 12:55:48'),
|
||||
(18, 'Olivia', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:56', '2021-03-04 12:55:56'),
|
||||
(19, 'yarnpkg', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:19', '2021-03-04 12:56:19'),
|
||||
(20, 'Qovery', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:25', '2021-03-04 12:56:25'),
|
||||
(21, 'The Design Collective', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:46', '2021-03-04 12:56:46'),
|
||||
(22, 'Tauri Apps', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:52', '2021-03-04 12:56:52'),
|
||||
(23, 'microsoft-python', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:06', '2021-03-04 12:57:06'),
|
||||
(24, 'AppBrewery', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:17', '2021-03-04 12:57:17'),
|
||||
(25, 'OpenSauced', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:23', '2021-03-04 12:57:23'),
|
||||
(26, 'Devsters', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:39', '2021-03-04 12:57:39'),
|
||||
(27, 'Coding Roads', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:49', '2021-03-04 12:57:49');
|
||||
|
||||
INSERT INTO `Channels` (`id`, `name`, `type`, `description`, `isDefault`, `guildId`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 'general', 'text', '', 1, 1, '2021-03-04 12:51:27', '2021-03-04 12:51:27'),
|
||||
(2, 'general', 'text', '', 1, 2, '2021-03-04 12:51:39', '2021-03-04 12:51:39'),
|
||||
(3, 'general', 'text', '', 1, 3, '2021-03-04 12:51:50', '2021-03-04 12:51:50'),
|
||||
(4, 'general', 'text', '', 1, 4, '2021-03-04 12:52:09', '2021-03-04 12:52:09'),
|
||||
(5, 'general', 'text', '', 1, 5, '2021-03-04 12:52:19', '2021-03-04 12:52:19'),
|
||||
(6, 'general', 'text', '', 1, 6, '2021-03-04 12:52:30', '2021-03-04 12:52:30'),
|
||||
(7, 'general', 'text', '', 1, 7, '2021-03-04 12:52:38', '2021-03-04 12:52:38'),
|
||||
(8, 'general', 'text', '', 1, 8, '2021-03-04 12:52:45', '2021-03-04 12:52:45'),
|
||||
(9, 'general', 'text', '', 1, 9, '2021-03-04 12:52:57', '2021-03-04 12:52:57'),
|
||||
(10, 'general', 'text', '', 1, 10, '2021-03-04 12:53:08', '2021-03-04 12:53:08'),
|
||||
(11, 'general', 'text', '', 1, 11, '2021-03-04 12:54:58', '2021-03-04 12:54:58'),
|
||||
(12, 'general', 'text', '', 1, 12, '2021-03-04 12:55:04', '2021-03-04 12:55:04'),
|
||||
(13, 'general', 'text', '', 1, 13, '2021-03-04 12:55:11', '2021-03-04 12:55:11'),
|
||||
(14, 'general', 'text', '', 1, 14, '2021-03-04 12:55:16', '2021-03-04 12:55:16'),
|
||||
(15, 'general', 'text', '', 1, 15, '2021-03-04 12:55:26', '2021-03-04 12:55:26'),
|
||||
(16, 'general', 'text', '', 1, 16, '2021-03-04 12:55:33', '2021-03-04 12:55:33'),
|
||||
(17, 'general', 'text', '', 1, 17, '2021-03-04 12:55:48', '2021-03-04 12:55:48'),
|
||||
(18, 'general', 'text', '', 1, 18, '2021-03-04 12:55:56', '2021-03-04 12:55:56'),
|
||||
(19, 'general', 'text', '', 1, 19, '2021-03-04 12:56:19', '2021-03-04 12:56:19'),
|
||||
(20, 'general', 'text', '', 1, 20, '2021-03-04 12:56:25', '2021-03-04 12:56:25'),
|
||||
(21, 'general', 'text', '', 1, 21, '2021-03-04 12:56:46', '2021-03-04 12:56:46'),
|
||||
(22, 'general', 'text', '', 1, 22, '2021-03-04 12:56:52', '2021-03-04 12:56:52'),
|
||||
(23, 'general', 'text', '', 1, 23, '2021-03-04 12:57:06', '2021-03-04 12:57:06'),
|
||||
(24, 'general', 'text', '', 1, 24, '2021-03-04 12:57:17', '2021-03-04 12:57:17'),
|
||||
(25, 'general', 'text', '', 1, 25, '2021-03-04 12:57:23', '2021-03-04 12:57:23'),
|
||||
(26, 'general', 'text', '', 1, 26, '2021-03-04 12:57:39', '2021-03-04 12:57:39'),
|
||||
(27, 'general', 'text', '', 1, 27, '2021-03-04 12:57:49', '2021-03-04 12:57:49');
|
||||
|
||||
INSERT INTO `Invitations` (`id`, `value`, `expiresIn`, `isPublic`, `guildId`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 'firstinvitation', 0, 1, 1, '2021-03-04 13:09:06', '2021-03-04 13:09:06');
|
||||
|
||||
INSERT INTO `Members` (`id`, `isOwner`, `lastVisitedChannelId`, `userId`, `guildId`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 1, 1, 1, 1, '2021-03-04 12:51:27', '2021-03-04 12:51:27'),
|
||||
(2, 1, 2, 1, 2, '2021-03-04 12:51:39', '2021-03-04 12:51:39'),
|
||||
(3, 1, 3, 1, 3, '2021-03-04 12:51:50', '2021-03-04 12:51:50'),
|
||||
(4, 1, 4, 1, 4, '2021-03-04 12:52:09', '2021-03-04 12:52:09'),
|
||||
(5, 1, 5, 1, 5, '2021-03-04 12:52:19', '2021-03-04 12:52:19'),
|
||||
(6, 1, 6, 1, 6, '2021-03-04 12:52:30', '2021-03-04 12:52:30'),
|
||||
(7, 1, 7, 1, 7, '2021-03-04 12:52:38', '2021-03-04 12:52:38'),
|
||||
(8, 1, 8, 1, 8, '2021-03-04 12:52:45', '2021-03-04 12:52:45'),
|
||||
(9, 1, 9, 1, 9, '2021-03-04 12:52:57', '2021-03-04 12:52:57'),
|
||||
(10, 1, 10, 1, 10, '2021-03-04 12:53:08', '2021-03-04 12:53:08'),
|
||||
(11, 1, 11, 1, 11, '2021-03-04 12:54:58', '2021-03-04 12:54:58'),
|
||||
(12, 1, 12, 1, 12, '2021-03-04 12:55:04', '2021-03-04 12:55:04'),
|
||||
(13, 1, 13, 1, 13, '2021-03-04 12:55:11', '2021-03-04 12:55:11'),
|
||||
(14, 1, 14, 1, 14, '2021-03-04 12:55:16', '2021-03-04 12:55:16'),
|
||||
(15, 1, 15, 1, 15, '2021-03-04 12:55:26', '2021-03-04 12:55:26'),
|
||||
(16, 1, 16, 1, 16, '2021-03-04 12:55:33', '2021-03-04 12:55:33'),
|
||||
(17, 1, 17, 1, 17, '2021-03-04 12:55:48', '2021-03-04 12:55:48'),
|
||||
(18, 1, 18, 1, 18, '2021-03-04 12:55:56', '2021-03-04 12:55:56'),
|
||||
(19, 1, 19, 1, 19, '2021-03-04 12:56:19', '2021-03-04 12:56:19'),
|
||||
(20, 1, 20, 1, 20, '2021-03-04 12:56:25', '2021-03-04 12:56:25'),
|
||||
(21, 1, 21, 1, 21, '2021-03-04 12:56:46', '2021-03-04 12:56:46'),
|
||||
(22, 1, 22, 1, 22, '2021-03-04 12:56:52', '2021-03-04 12:56:52'),
|
||||
(23, 1, 23, 1, 23, '2021-03-04 12:57:06', '2021-03-04 12:57:06'),
|
||||
(24, 1, 24, 1, 24, '2021-03-04 12:57:17', '2021-03-04 12:57:17'),
|
||||
(25, 1, 25, 1, 25, '2021-03-04 12:57:23', '2021-03-04 12:57:23'),
|
||||
(26, 1, 26, 1, 26, '2021-03-04 12:57:39', '2021-03-04 12:57:39'),
|
||||
(27, 1, 27, 1, 27, '2021-03-04 12:57:49', '2021-03-04 12:57:49');
|
||||
|
||||
INSERT INTO `Messages` (`id`, `value`, `type`, `mimetype`, `memberId`, `channelId`, `createdAt`, `updatedAt`) VALUES
|
||||
(1, 'Hello world!', 'text', 'text/plain', 1, 1, '2021-03-04 13:08:22', '2021-03-04 13:08:22');
|
49
src/models/utils.ts
Normal file
49
src/models/utils.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
export const date = {
|
||||
createdAt: Type.String({
|
||||
format: 'date-time',
|
||||
description: 'Created date time'
|
||||
}),
|
||||
updatedAt: Type.String({
|
||||
format: 'date-time',
|
||||
description: 'Last updated date time'
|
||||
})
|
||||
}
|
||||
|
||||
export const id = Type.Integer({ minimum: 1, description: 'Unique identifier' })
|
||||
|
||||
export const redirectURI = Type.String({ format: 'uri-reference' })
|
||||
|
||||
export const fastifyErrors = {
|
||||
400: Type.Object({
|
||||
statusCode: Type.Literal(400),
|
||||
error: Type.Literal('Bad Request'),
|
||||
message: Type.String()
|
||||
}),
|
||||
401: Type.Object({
|
||||
statusCode: Type.Literal(401),
|
||||
error: Type.Literal('Unauthorized'),
|
||||
message: Type.Literal('Unauthorized')
|
||||
}),
|
||||
403: Type.Object({
|
||||
statusCode: Type.Literal(403),
|
||||
error: Type.Literal('Forbidden'),
|
||||
message: Type.Literal('Forbidden')
|
||||
}),
|
||||
404: Type.Object({
|
||||
statusCode: Type.Literal(404),
|
||||
error: Type.Literal('Not Found'),
|
||||
message: Type.Literal('Not Found')
|
||||
}),
|
||||
431: Type.Object({
|
||||
statusCode: Type.Literal(431),
|
||||
error: Type.Literal('Request Header Fields Too Large'),
|
||||
message: Type.String()
|
||||
}),
|
||||
500: {
|
||||
statusCode: Type.Literal(500),
|
||||
error: Type.Literal('Internal Server Error'),
|
||||
message: Type.Literal('Something went wrong')
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: 'http'
|
||||
scheme: 'bearer'
|
||||
bearerFormat: 'JWT'
|
@ -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'
|
@ -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
|
@ -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'
|
@ -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'
|
@ -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])
|
||||
)
|
||||
})
|
||||
})
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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'
|
@ -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'
|
@ -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()
|
||||
})
|
||||
})
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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() }
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
@ -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)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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'
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
@ -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))
|
@ -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'
|
@ -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'
|
@ -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'
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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 })
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
@ -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'
|
@ -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'
|
@ -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)
|
||||
})
|
||||
})
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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)
|
||||
}
|
||||
)
|
@ -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)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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)
|
@ -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'
|
@ -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'
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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)
|
||||
}
|
||||
)
|
@ -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)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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'
|
@ -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()
|
||||
})
|
||||
})
|
@ -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() }
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
@ -1,7 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { getMembersRouter } from './get'
|
||||
|
||||
export const guildsMembersRouter = Router()
|
||||
|
||||
guildsMembersRouter.use('/', getMembersRouter)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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'
|
@ -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'
|
@ -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'
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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'
|
@ -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')
|
||||
})
|
||||
})
|
@ -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
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
@ -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)
|
||||
}
|
||||
|
@ -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'
|
@ -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'
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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)
|
@ -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 })
|
||||
}
|
||||
)
|
@ -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'
|
@ -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
|
||||
}
|
@ -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)
|
@ -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'
|
@ -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']))
|
||||
})
|
||||
})
|
@ -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 })
|
||||
}
|
||||
)
|
@ -1,7 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { joinInvitationsRouter } from './get'
|
||||
|
||||
export const invitationsJoinByValueRouter = Router()
|
||||
|
||||
invitationsJoinByValueRouter.use('/', joinInvitationsRouter)
|
@ -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'
|
@ -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'
|
@ -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)
|
||||
})
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user