feat(services): add POST /channels/[channelId]/messages/uploads
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -33,4 +33,4 @@ npm-debug.log* | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| uploads | ||||
| /uploads | ||||
|   | ||||
							
								
								
									
										2
									
								
								.swcrc
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								.swcrc
									
									
									
									
									
								
							| @@ -14,7 +14,7 @@ | ||||
|   }, | ||||
|   "module": { | ||||
|     "type": "commonjs", | ||||
|     "strict": true, | ||||
|     "strict": false, | ||||
|     "strictMode": true, | ||||
|     "lazy": false, | ||||
|     "noInterop": false | ||||
|   | ||||
							
								
								
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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": { | ||||
|   | ||||
| @@ -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" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -22,7 +22,8 @@ const bodyPostServiceSchema = Type.Object({ | ||||
| type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> | ||||
|  | ||||
| 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 | ||||
|   | ||||
							
								
								
									
										121
									
								
								src/services/channels/[channelId]/messages/uploads/post.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/services/channels/[channelId]/messages/uploads/post.ts
									
									
									
									
									
										Normal 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 | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -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<typeof parametersUploadsSchema> | ||||
|  | ||||
| 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)) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
							
								
								
									
										69
									
								
								src/tools/utils/uploadFile.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/tools/utils/uploadFile.ts
									
									
									
									
									
										Normal 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 } | ||||
| } | ||||
| @@ -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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user