feat(services): add PUT /guilds/[guildId]/icon
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -33,4 +33,3 @@ npm-debug.log*
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# misc
 | 
					# misc
 | 
				
			||||||
.DS_Store
 | 
					.DS_Store
 | 
				
			||||||
uploads
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,12 +9,12 @@ generator client {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
model User {
 | 
					model User {
 | 
				
			||||||
  id                       Int            @id @default(autoincrement())
 | 
					  id                       Int            @id @default(autoincrement())
 | 
				
			||||||
  name                     String         @unique @db.VarChar(255)
 | 
					  name                     String         @unique @db.VarChar(30)
 | 
				
			||||||
  email                    String?        @unique @db.VarChar(255)
 | 
					  email                    String?        @unique @db.VarChar(254)
 | 
				
			||||||
  password                 String?        @db.Text
 | 
					  password                 String?        @db.Text
 | 
				
			||||||
  logo                     String?        @db.Text
 | 
					  logo                     String?        @db.Text
 | 
				
			||||||
  status                   String?        @db.VarChar(255)
 | 
					  status                   String?        @db.VarChar(50)
 | 
				
			||||||
  biography                String?        @db.Text
 | 
					  biography                String?        @db.VarChar(160)
 | 
				
			||||||
  website                  String?        @db.VarChar(255)
 | 
					  website                  String?        @db.VarChar(255)
 | 
				
			||||||
  isConfirmed              Boolean        @default(false)
 | 
					  isConfirmed              Boolean        @default(false)
 | 
				
			||||||
  temporaryToken           String?
 | 
					  temporaryToken           String?
 | 
				
			||||||
@@ -29,8 +29,8 @@ model User {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
model UserSetting {
 | 
					model UserSetting {
 | 
				
			||||||
  id             Int      @id @default(autoincrement())
 | 
					  id             Int      @id @default(autoincrement())
 | 
				
			||||||
  language       String   @default("en") @db.VarChar(255)
 | 
					  language       String   @default("en") @db.VarChar(10)
 | 
				
			||||||
  theme          String   @default("dark") @db.VarChar(255)
 | 
					  theme          String   @default("dark") @db.VarChar(10)
 | 
				
			||||||
  isPublicEmail  Boolean  @default(false)
 | 
					  isPublicEmail  Boolean  @default(false)
 | 
				
			||||||
  isPublicGuilds Boolean  @default(false)
 | 
					  isPublicGuilds Boolean  @default(false)
 | 
				
			||||||
  createdAt      DateTime @default(now())
 | 
					  createdAt      DateTime @default(now())
 | 
				
			||||||
@@ -51,7 +51,7 @@ model RefreshToken {
 | 
				
			|||||||
model OAuth {
 | 
					model OAuth {
 | 
				
			||||||
  id         Int      @id @default(autoincrement())
 | 
					  id         Int      @id @default(autoincrement())
 | 
				
			||||||
  providerId String   @db.Text
 | 
					  providerId String   @db.Text
 | 
				
			||||||
  provider   String   @db.VarChar(255)
 | 
					  provider   String   @db.VarChar(20)
 | 
				
			||||||
  createdAt  DateTime @default(now())
 | 
					  createdAt  DateTime @default(now())
 | 
				
			||||||
  updatedAt  DateTime @default(now()) @updatedAt
 | 
					  updatedAt  DateTime @default(now()) @updatedAt
 | 
				
			||||||
  userId     Int      @unique
 | 
					  userId     Int      @unique
 | 
				
			||||||
@@ -72,7 +72,7 @@ model Member {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
model Guild {
 | 
					model Guild {
 | 
				
			||||||
  id          Int       @id @default(autoincrement())
 | 
					  id          Int       @id @default(autoincrement())
 | 
				
			||||||
  name        String    @db.VarChar(255)
 | 
					  name        String    @db.VarChar(30)
 | 
				
			||||||
  icon        String?   @db.Text
 | 
					  icon        String?   @db.Text
 | 
				
			||||||
  description String?   @db.Text
 | 
					  description String?   @db.Text
 | 
				
			||||||
  createdAt   DateTime  @default(now())
 | 
					  createdAt   DateTime  @default(now())
 | 
				
			||||||
@@ -83,7 +83,7 @@ model Guild {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
model Channel {
 | 
					model Channel {
 | 
				
			||||||
  id        Int       @id @default(autoincrement())
 | 
					  id        Int       @id @default(autoincrement())
 | 
				
			||||||
  name      String    @db.VarChar(255)
 | 
					  name      String    @db.VarChar(20)
 | 
				
			||||||
  createdAt DateTime  @default(now())
 | 
					  createdAt DateTime  @default(now())
 | 
				
			||||||
  updatedAt DateTime  @default(now()) @updatedAt
 | 
					  updatedAt DateTime  @default(now()) @updatedAt
 | 
				
			||||||
  guildId   Int       @unique
 | 
					  guildId   Int       @unique
 | 
				
			||||||
@@ -94,8 +94,8 @@ model Channel {
 | 
				
			|||||||
model Message {
 | 
					model Message {
 | 
				
			||||||
  id        Int      @id @default(autoincrement())
 | 
					  id        Int      @id @default(autoincrement())
 | 
				
			||||||
  value     String   @db.Text
 | 
					  value     String   @db.Text
 | 
				
			||||||
  type      String   @default("text") @db.VarChar(255)
 | 
					  type      String   @default("text") @db.VarChar(10)
 | 
				
			||||||
  mimetype  String   @default("text/plain") @db.VarChar(255)
 | 
					  mimetype  String   @default("text/plain") @db.VarChar(127)
 | 
				
			||||||
  createdAt DateTime @default(now())
 | 
					  createdAt DateTime @default(now())
 | 
				
			||||||
  updatedAt DateTime @default(now()) @updatedAt
 | 
					  updatedAt DateTime @default(now()) @updatedAt
 | 
				
			||||||
  memberId  Int      @unique
 | 
					  memberId  Int      @unique
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ export const types = [Type.Literal('text')]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const channelSchema = {
 | 
					export const channelSchema = {
 | 
				
			||||||
  id,
 | 
					  id,
 | 
				
			||||||
  name: Type.String({ maxLength: 255 }),
 | 
					  name: Type.String({ minLength: 1, maxLength: 20 }),
 | 
				
			||||||
  createdAt: date.createdAt,
 | 
					  createdAt: date.createdAt,
 | 
				
			||||||
  updatedAt: date.updatedAt,
 | 
					  updatedAt: date.updatedAt,
 | 
				
			||||||
  guildId: id
 | 
					  guildId: id
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,9 +5,9 @@ import { date, id } from './utils.js'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const guildSchema = {
 | 
					export const guildSchema = {
 | 
				
			||||||
  id,
 | 
					  id,
 | 
				
			||||||
  name: Type.String({ minLength: 3, maxLength: 30 }),
 | 
					  name: Type.String({ minLength: 1, maxLength: 30 }),
 | 
				
			||||||
  icon: Type.String({ format: 'uri-reference' }),
 | 
					  icon: Type.Union([Type.String({ format: 'uri-reference' }), Type.Null()]),
 | 
				
			||||||
  description: Type.String({ maxLength: 160 }),
 | 
					  description: Type.Union([Type.String({ maxLength: 160 }), Type.Null()]),
 | 
				
			||||||
  createdAt: date.createdAt,
 | 
					  createdAt: date.createdAt,
 | 
				
			||||||
  updatedAt: date.updatedAt
 | 
					  updatedAt: date.updatedAt
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,10 +6,13 @@ export const types = [Type.Literal('text'), Type.Literal('file')]
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const messageSchema = {
 | 
					export const messageSchema = {
 | 
				
			||||||
  id,
 | 
					  id,
 | 
				
			||||||
  value: Type.String(),
 | 
					  value: Type.String({
 | 
				
			||||||
 | 
					    minLength: 1,
 | 
				
			||||||
 | 
					    maxLength: 20_000
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
  type: Type.Union(types, { default: 'text' }),
 | 
					  type: Type.Union(types, { default: 'text' }),
 | 
				
			||||||
  mimetype: Type.String({
 | 
					  mimetype: Type.String({
 | 
				
			||||||
    maxLength: 255,
 | 
					    maxLength: 127,
 | 
				
			||||||
    default: 'text/plain',
 | 
					    default: 'text/plain',
 | 
				
			||||||
    format: 'mimetype'
 | 
					    format: 'mimetype'
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,11 +19,11 @@ export interface UserRequest {
 | 
				
			|||||||
export const userSchema = {
 | 
					export const userSchema = {
 | 
				
			||||||
  id,
 | 
					  id,
 | 
				
			||||||
  name: Type.String({ minLength: 1, maxLength: 30 }),
 | 
					  name: Type.String({ minLength: 1, maxLength: 30 }),
 | 
				
			||||||
  email: Type.String({ minLength: 1, maxLength: 255, format: 'email' }),
 | 
					  email: Type.String({ minLength: 1, maxLength: 254, format: 'email' }),
 | 
				
			||||||
  password: Type.String(),
 | 
					  password: Type.String(),
 | 
				
			||||||
  logo: Type.String({ format: 'uri-reference' }),
 | 
					  logo: Type.String({ format: 'uri-reference' }),
 | 
				
			||||||
  status: Type.String({ maxLength: 255 }),
 | 
					  status: Type.String({ maxLength: 50 }),
 | 
				
			||||||
  biography: Type.String(),
 | 
					  biography: Type.String({ maxLength: 160 }),
 | 
				
			||||||
  website: Type.String({ maxLength: 255, format: 'uri-reference' }),
 | 
					  website: Type.String({ maxLength: 255, format: 'uri-reference' }),
 | 
				
			||||||
  isConfirmed: Type.Boolean({ default: false }),
 | 
					  isConfirmed: Type.Boolean({ default: false }),
 | 
				
			||||||
  temporaryToken: Type.String(),
 | 
					  temporaryToken: Type.String(),
 | 
				
			||||||
@@ -32,18 +32,22 @@ export const userSchema = {
 | 
				
			|||||||
  updatedAt: date.updatedAt
 | 
					  updatedAt: date.updatedAt
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const userPublicSchema = {
 | 
					export const userPublicWithoutSettingsSchema = {
 | 
				
			||||||
  id,
 | 
					  id,
 | 
				
			||||||
  name: userSchema.name,
 | 
					  name: userSchema.name,
 | 
				
			||||||
  email: Type.Optional(userSchema.email),
 | 
					  email: Type.Union([userSchema.email, Type.Null()]),
 | 
				
			||||||
  logo: Type.Optional(userSchema.logo),
 | 
					  logo: Type.Union([userSchema.logo, Type.Null()]),
 | 
				
			||||||
  status: Type.Optional(userSchema.status),
 | 
					  status: Type.Union([userSchema.status, Type.Null()]),
 | 
				
			||||||
  biography: Type.Optional(userSchema.biography),
 | 
					  biography: Type.Union([userSchema.biography, Type.Null()]),
 | 
				
			||||||
  website: Type.Optional(userSchema.website),
 | 
					  website: Type.Union([userSchema.website, Type.Null()]),
 | 
				
			||||||
  isConfirmed: userSchema.isConfirmed,
 | 
					  isConfirmed: userSchema.isConfirmed,
 | 
				
			||||||
  createdAt: date.createdAt,
 | 
					  createdAt: date.createdAt,
 | 
				
			||||||
  updatedAt: date.updatedAt,
 | 
					  updatedAt: date.updatedAt
 | 
				
			||||||
  settings: Type.Optional(Type.Object(userSettingsSchema))
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const userPublicSchema = {
 | 
				
			||||||
 | 
					  ...userPublicWithoutSettingsSchema,
 | 
				
			||||||
 | 
					  settings: Type.Object(userSettingsSchema)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const userCurrentSchema = Type.Object({
 | 
					export const userCurrentSchema = Type.Object({
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										80
									
								
								src/services/guilds/[guildId]/icon/put.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/services/guilds/[guildId]/icon/put.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
				
			|||||||
 | 
					import { Static, Type } from '@sinclair/typebox'
 | 
				
			||||||
 | 
					import { FastifyPluginAsync, FastifySchema } from 'fastify'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 { guildSchema } from '../../../../models/Guild.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const parametersSchema = Type.Object({
 | 
				
			||||||
 | 
					  guildId: guildSchema.id
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Parameters = Static<typeof parametersSchema>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const putServiceSchema: FastifySchema = {
 | 
				
			||||||
 | 
					  description: 'Edit the icon of the guild with its id',
 | 
				
			||||||
 | 
					  tags: ['guilds'] 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({
 | 
				
			||||||
 | 
					      guild: Type.Object({
 | 
				
			||||||
 | 
					        icon: Type.String()
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					    400: fastifyErrors[400],
 | 
				
			||||||
 | 
					    401: fastifyErrors[401],
 | 
				
			||||||
 | 
					    403: fastifyErrors[403],
 | 
				
			||||||
 | 
					    404: fastifyErrors[404],
 | 
				
			||||||
 | 
					    431: fastifyErrors[431],
 | 
				
			||||||
 | 
					    500: fastifyErrors[500]
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					} as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
 | 
				
			||||||
 | 
					  await fastify.register(authenticateUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  await fastify.register(fastifyMultipart)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  fastify.route<{
 | 
				
			||||||
 | 
					    Params: Parameters
 | 
				
			||||||
 | 
					  }>({
 | 
				
			||||||
 | 
					    method: 'PUT',
 | 
				
			||||||
 | 
					    url: '/guilds/:guildId/icon',
 | 
				
			||||||
 | 
					    schema: putServiceSchema,
 | 
				
			||||||
 | 
					    handler: async (request, reply) => {
 | 
				
			||||||
 | 
					      if (request.user == null) {
 | 
				
			||||||
 | 
					        throw fastify.httpErrors.forbidden()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const { guildId } = request.params
 | 
				
			||||||
 | 
					      const guild = await prisma.guild.findUnique({ where: { id: guildId } })
 | 
				
			||||||
 | 
					      if (guild == null) {
 | 
				
			||||||
 | 
					        throw fastify.httpErrors.notFound()
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const icon = await uploadImage({
 | 
				
			||||||
 | 
					        fastify,
 | 
				
			||||||
 | 
					        request,
 | 
				
			||||||
 | 
					        folderInUploadsFolder: 'guilds'
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      await prisma.guild.update({
 | 
				
			||||||
 | 
					        where: { id: guildId },
 | 
				
			||||||
 | 
					        data: { icon }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      reply.statusCode = 200
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        guild: {
 | 
				
			||||||
 | 
					          icon
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
import { FastifyPluginAsync } from 'fastify'
 | 
					import { FastifyPluginAsync } from 'fastify'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { postGuilds } from './post.js'
 | 
					import { postGuilds } from './post.js'
 | 
				
			||||||
 | 
					import { putGuildIconById } from './[guildId]/icon/put.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const guildsService: FastifyPluginAsync = async (fastify) => {
 | 
					export const guildsService: FastifyPluginAsync = async (fastify) => {
 | 
				
			||||||
  await fastify.register(postGuilds)
 | 
					  await fastify.register(postGuilds)
 | 
				
			||||||
 | 
					  await fastify.register(putGuildIconById)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,8 @@ import authenticateUser from '../../tools/plugins/authenticateUser.js'
 | 
				
			|||||||
import { guildSchema } from '../../models/Guild.js'
 | 
					import { guildSchema } from '../../models/Guild.js'
 | 
				
			||||||
import { channelSchema } from '../../models/Channel.js'
 | 
					import { channelSchema } from '../../models/Channel.js'
 | 
				
			||||||
import { memberSchema } from '../../models/Member.js'
 | 
					import { memberSchema } from '../../models/Member.js'
 | 
				
			||||||
import { userPublicSchema } from '../../models/User.js'
 | 
					import { userPublicWithoutSettingsSchema } from '../../models/User.js'
 | 
				
			||||||
 | 
					import { parseStringNullish } from '../../tools/utils/parseStringNullish.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const bodyPostServiceSchema = Type.Object({
 | 
					const bodyPostServiceSchema = Type.Object({
 | 
				
			||||||
  name: guildSchema.name,
 | 
					  name: guildSchema.name,
 | 
				
			||||||
@@ -33,7 +34,7 @@ const postServiceSchema: FastifySchema = {
 | 
				
			|||||||
        members: Type.Array(
 | 
					        members: Type.Array(
 | 
				
			||||||
          Type.Object({
 | 
					          Type.Object({
 | 
				
			||||||
            ...memberSchema,
 | 
					            ...memberSchema,
 | 
				
			||||||
            user: Type.Object(userPublicSchema)
 | 
					            user: Type.Object(userPublicWithoutSettingsSchema)
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
@@ -59,7 +60,9 @@ export const postGuilds: FastifyPluginAsync = async (fastify) => {
 | 
				
			|||||||
        throw fastify.httpErrors.forbidden()
 | 
					        throw fastify.httpErrors.forbidden()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const { name, description } = request.body
 | 
					      const { name, description } = request.body
 | 
				
			||||||
      const guild = await prisma.guild.create({ data: { name, description } })
 | 
					      const guild = await prisma.guild.create({
 | 
				
			||||||
 | 
					        data: { name, description: parseStringNullish(description) }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
      const channel = await prisma.channel.create({
 | 
					      const channel = await prisma.channel.create({
 | 
				
			||||||
        data: { name: 'general', guildId: guild.id }
 | 
					        data: { name: 'general', guildId: guild.id }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ const parametersGetUserSchema = Type.Object({
 | 
				
			|||||||
  userId: userPublicSchema.id
 | 
					  userId: userPublicSchema.id
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ParametersGetUser = Static<typeof parametersGetUserSchema>
 | 
					type ParametersGetUser = Static<typeof parametersGetUserSchema>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getServiceSchema: FastifySchema = {
 | 
					const getServiceSchema: FastifySchema = {
 | 
				
			||||||
  description: 'GET the public user informations with its id',
 | 
					  description: 'GET the public user informations with its id',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,11 @@
 | 
				
			|||||||
import fs from 'node:fs'
 | 
					 | 
				
			||||||
import { URL } from 'node:url'
 | 
					 | 
				
			||||||
import { randomUUID } from 'node:crypto'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Type } from '@sinclair/typebox'
 | 
					import { Type } from '@sinclair/typebox'
 | 
				
			||||||
import { FastifyPluginAsync, FastifySchema } from 'fastify'
 | 
					import { FastifyPluginAsync, FastifySchema } from 'fastify'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
 | 
					import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
 | 
				
			||||||
import { fastifyErrors } from '../../../../models/utils.js'
 | 
					import { fastifyErrors } from '../../../../models/utils.js'
 | 
				
			||||||
import fastifyMultipart, { Multipart } from 'fastify-multipart'
 | 
					import fastifyMultipart from 'fastify-multipart'
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  maximumImageSize,
 | 
					 | 
				
			||||||
  supportedImageMimetype,
 | 
					 | 
				
			||||||
  ROOT_URL
 | 
					 | 
				
			||||||
} from '../../../../tools/configurations'
 | 
					 | 
				
			||||||
import prisma from '../../../../tools/database/prisma.js'
 | 
					import prisma from '../../../../tools/database/prisma.js'
 | 
				
			||||||
 | 
					import { uploadImage } from '../../../../tools/utils/uploadImage.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const putServiceSchema: FastifySchema = {
 | 
					const putServiceSchema: FastifySchema = {
 | 
				
			||||||
  description: 'Edit the current connected user logo',
 | 
					  description: 'Edit the current connected user logo',
 | 
				
			||||||
@@ -52,36 +44,11 @@ export const putCurrentUserLogo: FastifyPluginAsync = async (fastify) => {
 | 
				
			|||||||
      if (request.user == null) {
 | 
					      if (request.user == null) {
 | 
				
			||||||
        throw fastify.httpErrors.forbidden()
 | 
					        throw fastify.httpErrors.forbidden()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      let files: Multipart[] = []
 | 
					      const logo = await uploadImage({
 | 
				
			||||||
      try {
 | 
					        fastify,
 | 
				
			||||||
        files = await request.saveRequestFiles({
 | 
					        request,
 | 
				
			||||||
          limits: {
 | 
					        folderInUploadsFolder: 'users'
 | 
				
			||||||
            files: 1,
 | 
					 | 
				
			||||||
            fileSize: maximumImageSize * 1024 * 1024
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      } catch (error) {
 | 
					 | 
				
			||||||
        throw fastify.httpErrors.requestHeaderFieldsTooLarge(
 | 
					 | 
				
			||||||
          `body.logo 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 logoPath = `uploads/users/${randomUUID()}.${imageExtension}`
 | 
					 | 
				
			||||||
      const logoURL = new URL(logoPath, ROOT_URL)
 | 
					 | 
				
			||||||
      const logo = `/${logoPath}`
 | 
					 | 
				
			||||||
      await fs.promises.copyFile(image.filepath, logoURL)
 | 
					 | 
				
			||||||
      await prisma.user.update({
 | 
					      await prisma.user.update({
 | 
				
			||||||
        where: { id: request.user.current.id },
 | 
					        where: { id: request.user.current.id },
 | 
				
			||||||
        data: { logo }
 | 
					        data: { logo }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ import { userCurrentSchema, userSchema } from '../../../models/User.js'
 | 
				
			|||||||
import { sendEmail } from '../../../tools/email/sendEmail.js'
 | 
					import { sendEmail } from '../../../tools/email/sendEmail.js'
 | 
				
			||||||
import { HOST, PORT } from '../../../tools/configurations/index.js'
 | 
					import { HOST, PORT } from '../../../tools/configurations/index.js'
 | 
				
			||||||
import { Language, Theme } from '../../../models/UserSettings.js'
 | 
					import { Language, Theme } from '../../../models/UserSettings.js'
 | 
				
			||||||
 | 
					import { parseStringNullish } from '../../../tools/utils/parseStringNullish.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const bodyPutServiceSchema = Type.Object({
 | 
					const bodyPutServiceSchema = Type.Object({
 | 
				
			||||||
  name: Type.Optional(userSchema.name),
 | 
					  name: Type.Optional(userSchema.name),
 | 
				
			||||||
@@ -117,9 +118,12 @@ export const putCurrentUser: FastifyPluginAsync = async (fastify) => {
 | 
				
			|||||||
        where: { id: request.user.current.id },
 | 
					        where: { id: request.user.current.id },
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          name: name ?? request.user.current.name,
 | 
					          name: name ?? request.user.current.name,
 | 
				
			||||||
          status: status ?? request.user.current.status,
 | 
					          status: parseStringNullish(request.user.current.status, status),
 | 
				
			||||||
          biography: biography ?? request.user.current.biography,
 | 
					          biography: parseStringNullish(
 | 
				
			||||||
          website: website ?? request.user.current.website
 | 
					            request.user.current.biography,
 | 
				
			||||||
 | 
					            biography
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          website: parseStringNullish(request.user.current.website, website)
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      reply.statusCode = 200
 | 
					      reply.statusCode = 200
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ import { OAuthStrategy } from '../OAuthStrategy.js'
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const oauthStrategy = new OAuthStrategy('discord')
 | 
					const oauthStrategy = new OAuthStrategy('discord')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('/utils/OAuthStrategy - callbackSignin', () => {
 | 
					describe('/tools/utils/OAuthStrategy - callbackSignin', () => {
 | 
				
			||||||
  it('should signup the user', async () => {
 | 
					  it('should signup the user', async () => {
 | 
				
			||||||
    const name = 'Martin'
 | 
					    const name = 'Martin'
 | 
				
			||||||
    const id = '12345'
 | 
					    const id = '12345'
 | 
				
			||||||
@@ -52,7 +52,7 @@ describe('/utils/OAuthStrategy - callbackSignin', () => {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('/utils/OAuthStrategy - callbackAddStrategy', () => {
 | 
					describe('/tools/utils/OAuthStrategy - callbackAddStrategy', () => {
 | 
				
			||||||
  it('should add the strategy to the user', async () => {
 | 
					  it('should add the strategy to the user', async () => {
 | 
				
			||||||
    const name = userExample.name
 | 
					    const name = userExample.name
 | 
				
			||||||
    const id = '12345'
 | 
					    const id = '12345'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import { buildQueryURL } from '../buildQueryURL.js'
 | 
					import { buildQueryURL } from '../buildQueryURL.js'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test('controllers/users/utils/buildQueryUrl', () => {
 | 
					test('/tools/utils/buildQueryUrl', () => {
 | 
				
			||||||
  expect(
 | 
					  expect(
 | 
				
			||||||
    buildQueryURL('http://localhost:8080', {
 | 
					    buildQueryURL('http://localhost:8080', {
 | 
				
			||||||
      test: 'query'
 | 
					      test: 'query'
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								src/tools/utils/__test__/parseStringNullish.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/tools/utils/__test__/parseStringNullish.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import { parseStringNullish } from '../parseStringNullish'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultString = 'defaultString'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('/tools/utils/parseStringNullish', () => {
 | 
				
			||||||
 | 
					  it('returns `null` if `string.length === 0`', () => {
 | 
				
			||||||
 | 
					    expect(parseStringNullish(defaultString, '')).toEqual(null)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('returns `defaultString` if `string == null`', () => {
 | 
				
			||||||
 | 
					    expect(parseStringNullish(defaultString)).toEqual(defaultString)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('returns `string` if `string.length > 0`', () => {
 | 
				
			||||||
 | 
					    expect(parseStringNullish(defaultString, 'string')).toEqual('string')
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/tools/utils/parseStringNullish.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/tools/utils/parseStringNullish.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Parse a nullish string:
 | 
				
			||||||
 | 
					 *  - if `string.length === 0`, it returns `null`
 | 
				
			||||||
 | 
					 *  - if `string == null`, it returns `defaultString`
 | 
				
			||||||
 | 
					 *  - if `string.length > 0`, it returns `string`
 | 
				
			||||||
 | 
					 * @param defaultString
 | 
				
			||||||
 | 
					 * @param string
 | 
				
			||||||
 | 
					 * @returns
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export const parseStringNullish = (
 | 
				
			||||||
 | 
					  defaultString: string | null,
 | 
				
			||||||
 | 
					  string?: string
 | 
				
			||||||
 | 
					): string | null => {
 | 
				
			||||||
 | 
					  if (string != null) {
 | 
				
			||||||
 | 
					    if (string.length > 0) {
 | 
				
			||||||
 | 
					      return string
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return defaultString
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								src/tools/utils/uploadImage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/tools/utils/uploadImage.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					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'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										0
									
								
								uploads/guilds/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								uploads/guilds/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 2.4 KiB  | 
							
								
								
									
										0
									
								
								uploads/users/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								uploads/users/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 9.1 KiB  | 
		Reference in New Issue
	
	Block a user