diff --git a/.gitignore b/.gitignore index af45a45..0d85514 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ npm-debug.log* # misc .DS_Store -uploads +/uploads diff --git a/.swcrc b/.swcrc index c7bad41..e8dc7e7 100644 --- a/.swcrc +++ b/.swcrc @@ -14,7 +14,7 @@ }, "module": { "type": "commonjs", - "strict": true, + "strict": false, "strictMode": true, "lazy": false, "noInterop": false diff --git a/package-lock.json b/package-lock.json index b849081..d6aed3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,7 @@ "prisma": "3.7.0", "rimraf": "3.0.2", "semantic-release": "18.0.1", - "typescript": "4.4.4" + "typescript": "4.5.4" }, "engines": { "node": ">=16.0.0", @@ -16576,9 +16576,9 @@ } }, "node_modules/typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", + "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -29672,9 +29672,9 @@ } }, "typescript": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz", - "integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", + "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", "dev": true }, "uc.micro": { diff --git a/package.json b/package.json index ab68b03..8c6bd73 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,6 @@ "prisma": "3.7.0", "rimraf": "3.0.2", "semantic-release": "18.0.1", - "typescript": "4.4.4" + "typescript": "4.5.4" } } diff --git a/src/services/channels/[channelId]/messages/post.ts b/src/services/channels/[channelId]/messages/post.ts index b239fe9..ded9adc 100644 --- a/src/services/channels/[channelId]/messages/post.ts +++ b/src/services/channels/[channelId]/messages/post.ts @@ -22,7 +22,8 @@ const bodyPostServiceSchema = Type.Object({ type BodyPostServiceSchemaType = Static 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[], security: [ { @@ -43,6 +44,7 @@ const postServiceSchema: FastifySchema = { 401: fastifyErrors[401], 403: fastifyErrors[403], 404: fastifyErrors[404], + 431: fastifyErrors[431], 500: fastifyErrors[500] } } as const diff --git a/src/services/channels/[channelId]/messages/uploads/post.ts b/src/services/channels/[channelId]/messages/uploads/post.ts new file mode 100644 index 0000000..b29f371 --- /dev/null +++ b/src/services/channels/[channelId]/messages/uploads/post.ts @@ -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 + +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 + } + } + } + } + }) +} diff --git a/src/services/channels/index.ts b/src/services/channels/index.ts index 8ada89b..181aee7 100644 --- a/src/services/channels/index.ts +++ b/src/services/channels/index.ts @@ -3,9 +3,11 @@ import { FastifyPluginAsync } from 'fastify' import { getChannelByIdService } from './[channelId]/get.js' import { getMessagesByChannelIdService } from './[channelId]/messages/get.js' import { postMessageByChannelIdService } from './[channelId]/messages/post.js' +import { postMessageUploadsByChannelIdService } from './[channelId]/messages/uploads/post.js' export const channelsService: FastifyPluginAsync = async (fastify) => { await fastify.register(getChannelByIdService) await fastify.register(getMessagesByChannelIdService) await fastify.register(postMessageByChannelIdService) + await fastify.register(postMessageUploadsByChannelIdService) } diff --git a/src/services/guilds/[guildId]/icon/put.ts b/src/services/guilds/[guildId]/icon/put.ts index 480214a..c700d8c 100644 --- a/src/services/guilds/[guildId]/icon/put.ts +++ b/src/services/guilds/[guildId]/icon/put.ts @@ -5,8 +5,12 @@ import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import { fastifyErrors } from '../../../../models/utils.js' import fastifyMultipart from 'fastify-multipart' 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 { + maximumImageSize, + supportedImageMimetype +} from '../../../../tools/configurations/index.js' const parametersSchema = Type.Object({ guildId: guildSchema.id @@ -60,19 +64,21 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => { if (guild == null) { throw fastify.httpErrors.notFound() } - const icon = await uploadImage({ + const file = await uploadFile({ fastify, request, - folderInUploadsFolder: 'guilds' + folderInUploadsFolder: 'guilds', + maximumFileSize: maximumImageSize, + supportedFileMimetype: supportedImageMimetype }) await prisma.guild.update({ where: { id: guildId }, - data: { icon } + data: { icon: file.pathToStoreInDatabase } }) reply.statusCode = 200 return { guild: { - icon + icon: file.pathToStoreInDatabase } } } diff --git a/src/services/uploads/index.ts b/src/services/uploads/index.ts index d325e33..3d4bcff 100644 --- a/src/services/uploads/index.ts +++ b/src/services/uploads/index.ts @@ -3,6 +3,8 @@ import path from 'node:path' import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Static, Type } from '@sinclair/typebox' +import { fastifyErrors } from '../../models/utils' + const parametersUploadsSchema = Type.Object({ image: Type.String() }) @@ -11,7 +13,16 @@ type ParametersUploadsSchemaType = Static const getUploadsSchema: FastifySchema = { 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 export const uploadsService: FastifyPluginAsync = async (fastify) => { @@ -24,4 +35,24 @@ export const uploadsService: FastifyPluginAsync = async (fastify) => { 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)) + } + }) } diff --git a/src/services/users/current/logo/put.ts b/src/services/users/current/logo/put.ts index f0369e5..d0b311f 100644 --- a/src/services/users/current/logo/put.ts +++ b/src/services/users/current/logo/put.ts @@ -5,7 +5,11 @@ import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import { fastifyErrors } from '../../../../models/utils.js' import fastifyMultipart from 'fastify-multipart' 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 = { description: 'Edit the current connected user logo', @@ -44,19 +48,21 @@ export const putCurrentUserLogo: FastifyPluginAsync = async (fastify) => { if (request.user == null) { throw fastify.httpErrors.forbidden() } - const logo = await uploadImage({ + const file = await uploadFile({ fastify, request, - folderInUploadsFolder: 'users' + folderInUploadsFolder: 'users', + maximumFileSize: maximumImageSize, + supportedFileMimetype: supportedImageMimetype }) await prisma.user.update({ where: { id: request.user.current.id }, - data: { logo } + data: { logo: file.pathToStoreInDatabase } }) reply.statusCode = 200 return { user: { - logo + logo: file.pathToStoreInDatabase } } } diff --git a/src/tools/utils/uploadFile.ts b/src/tools/utils/uploadFile.ts new file mode 100644 index 0000000..cddd132 --- /dev/null +++ b/src/tools/utils/uploadFile.ts @@ -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 => { + 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 } +} diff --git a/src/tools/utils/uploadImage.ts b/src/tools/utils/uploadImage.ts deleted file mode 100644 index d64de92..0000000 --- a/src/tools/utils/uploadImage.ts +++ /dev/null @@ -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 => { - 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 -}