chore: initial commit

This commit is contained in:
Divlo
2022-04-08 17:54:56 +02:00
commit 7d8b88d5d2
49 changed files with 20473 additions and 0 deletions

38
src/application.ts Normal file
View 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
View 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
View 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
View 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)
}

View 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))
}
})
}

View 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
}
})
}

View 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)
}

View 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))
}
})
}

View 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
}
})
}

View 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))
}
})
}

View 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
}
})
}

View 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

View 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
}

View 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 }
}