chore: initial commit
This commit is contained in:
38
src/application.ts
Normal file
38
src/application.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import fastify from 'fastify'
|
||||
import fastifyCors from 'fastify-cors'
|
||||
import fastifySwagger from 'fastify-swagger'
|
||||
import fastifyHelmet from 'fastify-helmet'
|
||||
import fastifyRateLimit from 'fastify-rate-limit'
|
||||
import fastifySensible from 'fastify-sensible'
|
||||
import fastifyStatic from 'fastify-static'
|
||||
|
||||
import { services } from './services/index.js'
|
||||
import { swaggerOptions } from './tools/configurations/swaggerOptions.js'
|
||||
import { UPLOADS_URL } from './tools/configurations/index.js'
|
||||
|
||||
dotenv.config()
|
||||
export const application = fastify({
|
||||
logger: process.env.NODE_ENV === 'development',
|
||||
ajv: {
|
||||
customOptions: {
|
||||
format: 'full'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await application.register(fastifyCors)
|
||||
await application.register(fastifySensible)
|
||||
await application.register(fastifyHelmet)
|
||||
await application.register(fastifyRateLimit, {
|
||||
max: 200,
|
||||
timeWindow: '1 minute'
|
||||
})
|
||||
await application.register(fastifyStatic, {
|
||||
root: fileURLToPath(UPLOADS_URL),
|
||||
prefix: '/uploads/'
|
||||
})
|
||||
await application.register(fastifySwagger, swaggerOptions)
|
||||
await application.register(services)
|
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { application } from './application.js'
|
||||
import { HOST, PORT } from './tools/configurations/index.js'
|
||||
|
||||
const address = await application.listen(PORT, HOST)
|
||||
console.log('\u001B[36m%s\u001B[0m', `🚀 Server listening at ${address}`)
|
43
src/models/utils.ts
Normal file
43
src/models/utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Type } from '@sinclair/typebox'
|
||||
|
||||
export const fastifyErrorsSchema = {
|
||||
400: {
|
||||
statusCode: Type.Literal(400),
|
||||
error: Type.Literal('Bad Request'),
|
||||
message: Type.String()
|
||||
},
|
||||
401: {
|
||||
statusCode: Type.Literal(401),
|
||||
error: Type.Literal('Unauthorized'),
|
||||
message: Type.Literal('Unauthorized')
|
||||
},
|
||||
403: {
|
||||
statusCode: Type.Literal(403),
|
||||
error: Type.Literal('Forbidden'),
|
||||
message: Type.Literal('Forbidden')
|
||||
},
|
||||
404: {
|
||||
statusCode: Type.Literal(404),
|
||||
error: Type.Literal('Not Found'),
|
||||
message: Type.Literal('Not Found')
|
||||
},
|
||||
431: {
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
export const fastifyErrors = {
|
||||
400: Type.Object(fastifyErrorsSchema[400]),
|
||||
401: Type.Object(fastifyErrorsSchema[401]),
|
||||
403: Type.Object(fastifyErrorsSchema[403]),
|
||||
404: Type.Object(fastifyErrorsSchema[404]),
|
||||
431: Type.Object(fastifyErrorsSchema[431]),
|
||||
500: Type.Object(fastifyErrorsSchema[500])
|
||||
}
|
7
src/services/index.ts
Normal file
7
src/services/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import { uploadsService } from './uploads/index.js'
|
||||
|
||||
export const services: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(uploadsService)
|
||||
}
|
40
src/services/uploads/guilds/get.ts
Normal file
40
src/services/uploads/guilds/get.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
|
||||
const parameters = Type.Object({
|
||||
file: Type.String()
|
||||
})
|
||||
|
||||
type Parameters = Static<typeof parameters>
|
||||
|
||||
export const getServiceSchema: FastifySchema = {
|
||||
tags: ['uploads'] as string[],
|
||||
params: parameters,
|
||||
response: {
|
||||
200: {
|
||||
type: 'string',
|
||||
format: 'binary'
|
||||
},
|
||||
400: fastifyErrors[400],
|
||||
404: fastifyErrors[404],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const getGuildsUploadsService: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.route<{
|
||||
Params: Parameters
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: '/uploads/guilds/:file',
|
||||
schema: getServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { file } = request.params
|
||||
return await reply.sendFile(path.join('guilds', file))
|
||||
}
|
||||
})
|
||||
}
|
46
src/services/uploads/guilds/post.ts
Normal file
46
src/services/uploads/guilds/post.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import fastifyMultipart from 'fastify-multipart'
|
||||
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { uploadFile } from '../../../tools/utils/uploadFile.js'
|
||||
import {
|
||||
MAXIMUM_IMAGE_SIZE,
|
||||
SUPPORTED_IMAGE_MIMETYPE
|
||||
} from '../../../tools/configurations/index.js'
|
||||
|
||||
const postServiceSchema: FastifySchema = {
|
||||
description: 'Uploads guild icon',
|
||||
tags: ['uploads'] as string[],
|
||||
consumes: ['multipart/form-data'] as string[],
|
||||
produces: ['application/json'] as string[],
|
||||
response: {
|
||||
201: Type.String(),
|
||||
400: fastifyErrors[400],
|
||||
431: fastifyErrors[431],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const postGuildsUploadsIconService: FastifyPluginAsync = async (
|
||||
fastify
|
||||
) => {
|
||||
await fastify.register(fastifyMultipart)
|
||||
|
||||
fastify.route({
|
||||
method: 'POST',
|
||||
url: '/uploads/guilds',
|
||||
schema: postServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const file = await uploadFile({
|
||||
fastify,
|
||||
request,
|
||||
folderInUploadsFolder: 'guilds',
|
||||
maximumFileSize: MAXIMUM_IMAGE_SIZE,
|
||||
supportedFileMimetype: SUPPORTED_IMAGE_MIMETYPE
|
||||
})
|
||||
reply.statusCode = 201
|
||||
return file.pathToStoreInDatabase
|
||||
}
|
||||
})
|
||||
}
|
19
src/services/uploads/index.ts
Normal file
19
src/services/uploads/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
|
||||
import { getGuildsUploadsService } from './guilds/get.js'
|
||||
import { postGuildsUploadsIconService } from './guilds/post.js'
|
||||
import { getMessagesUploadsService } from './messages/get.js'
|
||||
import { postMessagesUploadsService } from './messages/post.js'
|
||||
import { getUsersUploadsService } from './users/get.js'
|
||||
import { postUsersUploadsLogoService } from './users/post.js'
|
||||
|
||||
export const uploadsService: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.register(getGuildsUploadsService)
|
||||
await fastify.register(postGuildsUploadsIconService)
|
||||
|
||||
await fastify.register(getMessagesUploadsService)
|
||||
await fastify.register(postMessagesUploadsService)
|
||||
|
||||
await fastify.register(getUsersUploadsService)
|
||||
await fastify.register(postUsersUploadsLogoService)
|
||||
}
|
42
src/services/uploads/messages/get.ts
Normal file
42
src/services/uploads/messages/get.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
|
||||
const parameters = Type.Object({
|
||||
file: Type.String()
|
||||
})
|
||||
|
||||
type Parameters = Static<typeof parameters>
|
||||
|
||||
export const getServiceSchema: FastifySchema = {
|
||||
tags: ['uploads'] as string[],
|
||||
params: parameters,
|
||||
response: {
|
||||
200: {
|
||||
type: 'string',
|
||||
format: 'binary'
|
||||
},
|
||||
400: fastifyErrors[400],
|
||||
404: fastifyErrors[404],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const getMessagesUploadsService: FastifyPluginAsync = async (
|
||||
fastify
|
||||
) => {
|
||||
fastify.route<{
|
||||
Params: Parameters
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: '/uploads/messages/:file',
|
||||
schema: getServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { file } = request.params
|
||||
return await reply.sendFile(path.join('messages', file))
|
||||
}
|
||||
})
|
||||
}
|
42
src/services/uploads/messages/post.ts
Normal file
42
src/services/uploads/messages/post.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import fastifyMultipart from 'fastify-multipart'
|
||||
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { uploadFile } from '../../../tools/utils/uploadFile.js'
|
||||
import { MAXIMUM_IMAGE_SIZE } from '../../../tools/configurations/index.js'
|
||||
|
||||
const postServiceSchema: FastifySchema = {
|
||||
description: 'Uploads message file',
|
||||
tags: ['uploads'] as string[],
|
||||
consumes: ['multipart/form-data'] as string[],
|
||||
produces: ['application/json'] as string[],
|
||||
response: {
|
||||
201: Type.String(),
|
||||
400: fastifyErrors[400],
|
||||
431: fastifyErrors[431],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const postMessagesUploadsService: FastifyPluginAsync = async (
|
||||
fastify
|
||||
) => {
|
||||
await fastify.register(fastifyMultipart)
|
||||
|
||||
fastify.route({
|
||||
method: 'POST',
|
||||
url: '/uploads/messages',
|
||||
schema: postServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const file = await uploadFile({
|
||||
fastify,
|
||||
request,
|
||||
folderInUploadsFolder: 'messages',
|
||||
maximumFileSize: MAXIMUM_IMAGE_SIZE
|
||||
})
|
||||
reply.statusCode = 201
|
||||
return file.pathToStoreInDatabase
|
||||
}
|
||||
})
|
||||
}
|
40
src/services/uploads/users/get.ts
Normal file
40
src/services/uploads/users/get.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import { Static, Type } from '@sinclair/typebox'
|
||||
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
|
||||
const parameters = Type.Object({
|
||||
file: Type.String()
|
||||
})
|
||||
|
||||
type Parameters = Static<typeof parameters>
|
||||
|
||||
export const getServiceSchema: FastifySchema = {
|
||||
tags: ['uploads'] as string[],
|
||||
params: parameters,
|
||||
response: {
|
||||
200: {
|
||||
type: 'string',
|
||||
format: 'binary'
|
||||
},
|
||||
400: fastifyErrors[400],
|
||||
404: fastifyErrors[404],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const getUsersUploadsService: FastifyPluginAsync = async (fastify) => {
|
||||
await fastify.route<{
|
||||
Params: Parameters
|
||||
}>({
|
||||
method: 'GET',
|
||||
url: '/uploads/users/:file',
|
||||
schema: getServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const { file } = request.params
|
||||
return await reply.sendFile(path.join('users', file))
|
||||
}
|
||||
})
|
||||
}
|
46
src/services/uploads/users/post.ts
Normal file
46
src/services/uploads/users/post.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Type } from '@sinclair/typebox'
|
||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||
import fastifyMultipart from 'fastify-multipart'
|
||||
|
||||
import { fastifyErrors } from '../../../models/utils.js'
|
||||
import { uploadFile } from '../../../tools/utils/uploadFile.js'
|
||||
import {
|
||||
MAXIMUM_IMAGE_SIZE,
|
||||
SUPPORTED_IMAGE_MIMETYPE
|
||||
} from '../../../tools/configurations/index.js'
|
||||
|
||||
const postServiceSchema: FastifySchema = {
|
||||
description: 'Uploads user logo',
|
||||
tags: ['uploads'] as string[],
|
||||
consumes: ['multipart/form-data'] as string[],
|
||||
produces: ['application/json'] as string[],
|
||||
response: {
|
||||
201: Type.String(),
|
||||
400: fastifyErrors[400],
|
||||
431: fastifyErrors[431],
|
||||
500: fastifyErrors[500]
|
||||
}
|
||||
} as const
|
||||
|
||||
export const postUsersUploadsLogoService: FastifyPluginAsync = async (
|
||||
fastify
|
||||
) => {
|
||||
await fastify.register(fastifyMultipart)
|
||||
|
||||
fastify.route({
|
||||
method: 'POST',
|
||||
url: '/uploads/users',
|
||||
schema: postServiceSchema,
|
||||
handler: async (request, reply) => {
|
||||
const file = await uploadFile({
|
||||
fastify,
|
||||
request,
|
||||
folderInUploadsFolder: 'users',
|
||||
maximumFileSize: MAXIMUM_IMAGE_SIZE,
|
||||
supportedFileMimetype: SUPPORTED_IMAGE_MIMETYPE
|
||||
})
|
||||
reply.statusCode = 201
|
||||
return file.pathToStoreInDatabase
|
||||
}
|
||||
})
|
||||
}
|
26
src/tools/configurations/index.ts
Normal file
26
src/tools/configurations/index.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { URL } from 'node:url'
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
export const PORT = parseInt(process.env.PORT ?? '8000', 10)
|
||||
export const HOST = process.env.HOST ?? '0.0.0.0'
|
||||
export const API_URL = process.env.API_URL ?? `http://${HOST}:${PORT}`
|
||||
|
||||
export const SRC_URL = new URL('../../', import.meta.url)
|
||||
export const ROOT_URL = new URL('../', SRC_URL)
|
||||
export const UPLOADS_URL = new URL('./uploads/', ROOT_URL)
|
||||
|
||||
export const SUPPORTED_IMAGE_MIMETYPE = [
|
||||
'image/png',
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
'image/gif'
|
||||
]
|
||||
|
||||
/** in megabytes */
|
||||
export const MAXIMUM_IMAGE_SIZE = 10
|
||||
|
||||
/** in megabytes */
|
||||
export const MAXIMUM_FILE_SIZE = 100
|
22
src/tools/configurations/swaggerOptions.ts
Normal file
22
src/tools/configurations/swaggerOptions.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import dotenv from 'dotenv'
|
||||
import { readPackage } from 'read-pkg'
|
||||
import { FastifyDynamicSwaggerOptions } from 'fastify-swagger'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
const packageJSON = await readPackage()
|
||||
|
||||
export const swaggerOptions: FastifyDynamicSwaggerOptions = {
|
||||
routePrefix: '/documentation',
|
||||
openapi: {
|
||||
info: {
|
||||
title: packageJSON.name,
|
||||
description: packageJSON.description,
|
||||
version: packageJSON.version
|
||||
},
|
||||
tags: [{ name: 'guilds' }, { name: 'messages' }, { name: 'users' }]
|
||||
},
|
||||
exposeRoute: true,
|
||||
staticCSP: true,
|
||||
hideUntagged: true
|
||||
}
|
68
src/tools/utils/uploadFile.ts
Normal file
68
src/tools/utils/uploadFile.ts
Normal file
@ -0,0 +1,68 @@
|
||||
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 { API_URL, 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 = `${API_URL}/${filePath}`
|
||||
await fs.promises.copyFile(file.filepath, fileURL)
|
||||
return { pathToStoreInDatabase, mimetype: file.mimetype }
|
||||
}
|
Reference in New Issue
Block a user