feat(services): add POST /channels/[channelId]/messages/uploads

This commit is contained in:
Divlo 2022-01-01 14:19:27 +00:00
parent 766c9fdbd6
commit 03946f26e7
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
12 changed files with 259 additions and 76 deletions

2
.gitignore vendored
View File

@ -33,4 +33,4 @@ npm-debug.log*
# misc # misc
.DS_Store .DS_Store
uploads /uploads

2
.swcrc
View File

@ -14,7 +14,7 @@
}, },
"module": { "module": {
"type": "commonjs", "type": "commonjs",
"strict": true, "strict": false,
"strictMode": true, "strictMode": true,
"lazy": false, "lazy": false,
"noInterop": false "noInterop": false

14
package-lock.json generated
View File

@ -73,7 +73,7 @@
"prisma": "3.7.0", "prisma": "3.7.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semantic-release": "18.0.1", "semantic-release": "18.0.1",
"typescript": "4.4.4" "typescript": "4.5.4"
}, },
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0",
@ -16576,9 +16576,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.4.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@ -29672,9 +29672,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "4.4.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
"dev": true "dev": true
}, },
"uc.micro": { "uc.micro": {

View File

@ -95,6 +95,6 @@
"prisma": "3.7.0", "prisma": "3.7.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"semantic-release": "18.0.1", "semantic-release": "18.0.1",
"typescript": "4.4.4" "typescript": "4.5.4"
} }
} }

View File

@ -22,7 +22,8 @@ const bodyPostServiceSchema = Type.Object({
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: 'POST a new message in a specific channel using its channelId.', description:
'POST a new message (text) in a specific channel using its channelId.',
tags: ['messages'] as string[], tags: ['messages'] as string[],
security: [ security: [
{ {
@ -43,6 +44,7 @@ const postServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431],
500: fastifyErrors[500] 500: fastifyErrors[500]
} }
} as const } as const

View File

@ -0,0 +1,121 @@
import { Type, Static } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify'
import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../../tools/database/prisma.js'
import { fastifyErrors } from '../../../../../models/utils.js'
import authenticateUser from '../../../../../tools/plugins/authenticateUser.js'
import { messageSchema } from '../../../../../models/Message.js'
import { memberSchema } from '../../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from '../../../../../models/User.js'
import { channelSchema } from '../../../../../models/Channel.js'
import { uploadFile } from '../../../../../tools/utils/uploadFile.js'
import { maximumFileSize } from '../../../../../tools/configurations/index.js'
const parametersSchema = Type.Object({
channelId: channelSchema.id
})
type Parameters = Static<typeof parametersSchema>
const postServiceSchema: FastifySchema = {
description:
'POST a new message (file) in a specific channel using its channelId.',
tags: ['messages'] as string[],
consumes: ['multipart/form-data'] as string[],
produces: ['application/json'] as string[],
security: [
{
bearerAuth: []
}
] as Array<{ [key: string]: [] }>,
params: parametersSchema,
response: {
200: Type.Object({
...messageSchema,
member: Type.Object({
...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema)
})
}),
400: fastifyErrors[400],
401: fastifyErrors[401],
403: fastifyErrors[403],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const
export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
fastify
) => {
await fastify.register(authenticateUser)
await fastify.register(fastifyMultipart)
fastify.route<{
Params: Parameters
}>({
method: 'POST',
url: '/channels/:channelId/messages/uploads',
schema: postServiceSchema,
handler: async (request, reply) => {
if (request.user == null) {
throw fastify.httpErrors.forbidden()
}
const { channelId } = request.params
const channel = await prisma.channel.findUnique({
where: { id: channelId }
})
if (channel == null) {
throw fastify.httpErrors.notFound('Channel not found')
}
const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id },
include: {
user: {
select: {
id: true,
name: true,
logo: true,
status: true,
biography: true,
website: true,
createdAt: true,
updatedAt: true
}
}
}
})
if (memberCheck == null) {
throw fastify.httpErrors.notFound('Channel not found')
}
const file = await uploadFile({
fastify,
request,
folderInUploadsFolder: 'messages',
maximumFileSize: maximumFileSize
})
const message = await prisma.message.create({
data: {
value: file.pathToStoreInDatabase,
type: 'file',
mimetype: file.mimetype,
channelId,
memberId: memberCheck.id
}
})
reply.statusCode = 201
return {
...message,
member: {
...memberCheck,
user: {
...memberCheck.user,
email: null
}
}
}
}
})
}

View File

@ -3,9 +3,11 @@ import { FastifyPluginAsync } from 'fastify'
import { getChannelByIdService } from './[channelId]/get.js' import { getChannelByIdService } from './[channelId]/get.js'
import { getMessagesByChannelIdService } from './[channelId]/messages/get.js' import { getMessagesByChannelIdService } from './[channelId]/messages/get.js'
import { postMessageByChannelIdService } from './[channelId]/messages/post.js' import { postMessageByChannelIdService } from './[channelId]/messages/post.js'
import { postMessageUploadsByChannelIdService } from './[channelId]/messages/uploads/post.js'
export const channelsService: FastifyPluginAsync = async (fastify) => { export const channelsService: FastifyPluginAsync = async (fastify) => {
await fastify.register(getChannelByIdService) await fastify.register(getChannelByIdService)
await fastify.register(getMessagesByChannelIdService) await fastify.register(getMessagesByChannelIdService)
await fastify.register(postMessageByChannelIdService) await fastify.register(postMessageByChannelIdService)
await fastify.register(postMessageUploadsByChannelIdService)
} }

View File

@ -5,8 +5,12 @@ import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart from 'fastify-multipart' import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../tools/database/prisma.js' import prisma from '../../../../tools/database/prisma.js'
import { uploadImage } from '../../../../tools/utils/uploadImage.js' import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import { guildSchema } from '../../../../models/Guild.js' import { guildSchema } from '../../../../models/Guild.js'
import {
maximumImageSize,
supportedImageMimetype
} from '../../../../tools/configurations/index.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id
@ -60,19 +64,21 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
if (guild == null) { if (guild == null) {
throw fastify.httpErrors.notFound() throw fastify.httpErrors.notFound()
} }
const icon = await uploadImage({ const file = await uploadFile({
fastify, fastify,
request, request,
folderInUploadsFolder: 'guilds' folderInUploadsFolder: 'guilds',
maximumFileSize: maximumImageSize,
supportedFileMimetype: supportedImageMimetype
}) })
await prisma.guild.update({ await prisma.guild.update({
where: { id: guildId }, where: { id: guildId },
data: { icon } data: { icon: file.pathToStoreInDatabase }
}) })
reply.statusCode = 200 reply.statusCode = 200
return { return {
guild: { guild: {
icon icon: file.pathToStoreInDatabase
} }
} }
} }

View File

@ -3,6 +3,8 @@ import path from 'node:path'
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { FastifyPluginAsync, FastifySchema } from 'fastify'
import { Static, Type } from '@sinclair/typebox' import { Static, Type } from '@sinclair/typebox'
import { fastifyErrors } from '../../models/utils'
const parametersUploadsSchema = Type.Object({ const parametersUploadsSchema = Type.Object({
image: Type.String() image: Type.String()
}) })
@ -11,7 +13,16 @@ type ParametersUploadsSchemaType = Static<typeof parametersUploadsSchema>
const getUploadsSchema: FastifySchema = { const getUploadsSchema: FastifySchema = {
tags: ['uploads'] as string[], tags: ['uploads'] as string[],
params: parametersUploadsSchema params: parametersUploadsSchema,
response: {
200: {
type: 'string',
format: 'binary'
},
400: fastifyErrors[400],
404: fastifyErrors[404],
500: fastifyErrors[500]
}
} as const } as const
export const uploadsService: FastifyPluginAsync = async (fastify) => { export const uploadsService: FastifyPluginAsync = async (fastify) => {
@ -24,4 +35,24 @@ export const uploadsService: FastifyPluginAsync = async (fastify) => {
return await reply.sendFile(path.join('users', image)) return await reply.sendFile(path.join('users', image))
} }
}) })
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/guilds/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('guilds', image))
}
})
fastify.route<{ Params: ParametersUploadsSchemaType }>({
method: 'GET',
url: '/uploads/messages/:image',
schema: getUploadsSchema,
handler: async (request, reply) => {
const { image } = request.params
return await reply.sendFile(path.join('messages', image))
}
})
} }

View File

@ -5,7 +5,11 @@ import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from '../../../../models/utils.js'
import fastifyMultipart from 'fastify-multipart' import fastifyMultipart from 'fastify-multipart'
import prisma from '../../../../tools/database/prisma.js' import prisma from '../../../../tools/database/prisma.js'
import { uploadImage } from '../../../../tools/utils/uploadImage.js' import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import {
maximumImageSize,
supportedImageMimetype
} from '../../../../tools/configurations/index.js'
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: 'Edit the current connected user logo', description: 'Edit the current connected user logo',
@ -44,19 +48,21 @@ export const putCurrentUserLogo: FastifyPluginAsync = async (fastify) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const logo = await uploadImage({ const file = await uploadFile({
fastify, fastify,
request, request,
folderInUploadsFolder: 'users' folderInUploadsFolder: 'users',
maximumFileSize: maximumImageSize,
supportedFileMimetype: supportedImageMimetype
}) })
await prisma.user.update({ await prisma.user.update({
where: { id: request.user.current.id }, where: { id: request.user.current.id },
data: { logo } data: { logo: file.pathToStoreInDatabase }
}) })
reply.statusCode = 200 reply.statusCode = 200
return { return {
user: { user: {
logo logo: file.pathToStoreInDatabase
} }
} }
} }

View File

@ -0,0 +1,69 @@
import fs from 'node:fs'
import { URL } from 'node:url'
import { randomUUID } from 'node:crypto'
import { FastifyInstance, FastifyRequest } from 'fastify'
import { Multipart } from 'fastify-multipart'
import { ROOT_URL } from '../configurations/index.js'
export interface UploadFileOptions {
folderInUploadsFolder: 'guilds' | 'messages' | 'users'
request: FastifyRequest
fastify: FastifyInstance
/** in megabytes */
maximumFileSize: number
supportedFileMimetype?: string[]
}
export interface UploadFileResult {
pathToStoreInDatabase: string
mimetype: string
}
export const uploadFile = async (
options: UploadFileOptions
): Promise<UploadFileResult> => {
const {
fastify,
request,
folderInUploadsFolder,
maximumFileSize,
supportedFileMimetype
} = options
let files: Multipart[] = []
try {
files = await request.saveRequestFiles({
limits: {
files: 1,
fileSize: maximumFileSize * 1024 * 1024
}
})
} catch (error) {
throw fastify.httpErrors.requestHeaderFieldsTooLarge(
`File should be less than ${maximumFileSize}mb.`
)
}
if (files.length !== 1) {
throw fastify.httpErrors.badRequest('You must upload at most one file.')
}
const file = files[0]
if (
supportedFileMimetype != null &&
!supportedFileMimetype.includes(file.mimetype)
) {
throw fastify.httpErrors.badRequest(
`The file must have a valid type (${supportedFileMimetype.join(', ')}).`
)
}
const splitedMimetype = file.mimetype.split('/')
const fileExtension = splitedMimetype[1]
const filePath = `uploads/${folderInUploadsFolder}/${randomUUID()}.${fileExtension}`
const fileURL = new URL(filePath, ROOT_URL)
const pathToStoreInDatabase = `/${filePath}`
await fs.promises.copyFile(file.filepath, fileURL)
return { pathToStoreInDatabase, mimetype: file.mimetype }
}

View File

@ -1,54 +0,0 @@
import fs from 'node:fs'
import { URL } from 'node:url'
import { randomUUID } from 'node:crypto'
import { FastifyInstance, FastifyRequest } from 'fastify'
import { Multipart } from 'fastify-multipart'
import {
maximumImageSize,
supportedImageMimetype,
ROOT_URL
} from '../configurations/index.js'
export interface UploadImageOptions {
folderInUploadsFolder: 'guilds' | 'messages' | 'users'
request: FastifyRequest
fastify: FastifyInstance
}
export const uploadImage = async (
options: UploadImageOptions
): Promise<string> => {
const { fastify, request, folderInUploadsFolder } = options
let files: Multipart[] = []
try {
files = await request.saveRequestFiles({
limits: {
files: 1,
fileSize: maximumImageSize * 1024 * 1024
}
})
} catch (error) {
throw fastify.httpErrors.requestHeaderFieldsTooLarge(
`Image should be less than ${maximumImageSize}mb.`
)
}
if (files.length !== 1) {
throw fastify.httpErrors.badRequest('You must upload at most one file.')
}
const image = files[0]
if (!supportedImageMimetype.includes(image.mimetype)) {
throw fastify.httpErrors.badRequest(
`The file must have a valid type (${supportedImageMimetype.join(', ')}).`
)
}
const splitedMimetype = image.mimetype.split('/')
const imageExtension = splitedMimetype[1]
const imagePath = `uploads/${folderInUploadsFolder}/${randomUUID()}.${imageExtension}`
const imageURL = new URL(imagePath, ROOT_URL)
const imagePathToStoreInDatabase = `/${imagePath}`
await fs.promises.copyFile(image.filepath, imageURL)
return imagePathToStoreInDatabase
}