diff --git a/api/prisma/migrations/20240728012747_add_mime_type_to_image/migration.sql b/api/prisma/migrations/20240728012747_add_mime_type_to_image/migration.sql new file mode 100644 index 0000000..656d7c6 --- /dev/null +++ b/api/prisma/migrations/20240728012747_add_mime_type_to_image/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Image" ADD COLUMN "mime_type" TEXT NOT NULL DEFAULT ''; diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 8a2d503..53d2259 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -85,9 +85,10 @@ model Comment { } model Image { - id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid - name String @unique - image Bytes + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String @unique + image Bytes + mimetype String @map("mime_type") @default("") } enum Role { diff --git a/api/src/image/daos/image.dao.ts b/api/src/image/daos/image.dao.ts index 7e7439b..21de2df 100644 --- a/api/src/image/daos/image.dao.ts +++ b/api/src/image/daos/image.dao.ts @@ -5,24 +5,31 @@ import { PrismaService } from '../../prisma/prisma.service'; export class ImageDao { constructor(private readonly prisma: PrismaService) {} - getImage(filename: string) { + getImage(username: string) { return this.prisma.image.findUniqueOrThrow({ where: { - name: filename, + name: username, }, }); } - createImage(filename: string, image: Buffer) { - return this.prisma.image.create({ - data: { name: filename, image }, + createImage(username: string, mimetype: string, image: Buffer) { + return this.prisma.image.upsert({ + where: { + name: username, + }, + update: { + mimetype, + image, + }, + create: { name: username, image, mimetype }, }); } - async deleteImage(filename: string) { - return await this.prisma.image.delete({ + deleteImage(username: string) { + this.prisma.image.delete({ where: { - name: filename, + name: username, }, }); } diff --git a/api/src/image/image.controller.ts b/api/src/image/image.controller.ts index 735bf36..df3fd8b 100644 --- a/api/src/image/image.controller.ts +++ b/api/src/image/image.controller.ts @@ -10,8 +10,8 @@ export class ImageController { @Public() @Get('/:name') async getImage(@Param('name') name: string, @Res() res: Response) { - const image = await this.imageService.getImage(name); + const { image, mimetype } = await this.imageService.getImage(name); - res.end(image); + res.contentType(mimetype).end(image); } } diff --git a/api/src/image/image.service.ts b/api/src/image/image.service.ts index d84c8fc..d3587e3 100644 --- a/api/src/image/image.service.ts +++ b/api/src/image/image.service.ts @@ -1,25 +1,19 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ImageDao } from './daos/image.dao'; @Injectable() export class ImageService { constructor(private imageDao: ImageDao) {} - async getImage(filename: string) { - const { image } = await this.imageDao.getImage(filename); - - if (!image) { - throw new NotFoundException('No image found'); - } - - return image; + async getImage(username: string) { + return this.imageDao.getImage(username); } - async createImage(filename: string, image: Buffer) { - return this.imageDao.createImage(filename, image); + async createImage(username: string, mimetype: string, image: Buffer) { + return this.imageDao.createImage(username, mimetype, image); } - async deleteImage(filename: string) { - return await this.imageDao.deleteImage(filename); + async deleteImage(username: string) { + return this.imageDao.deleteImage(username); } } diff --git a/api/src/user/user.controller.spec.ts b/api/src/user/user.controller.spec.ts index 2fa1a9c..a275aaf 100644 --- a/api/src/user/user.controller.spec.ts +++ b/api/src/user/user.controller.spec.ts @@ -97,8 +97,13 @@ describe('UserController', () => { }, }; fakeImageService = { - async getImage(name: string) { - return Buffer.from(name); + getImage: (name: string) => { + return Promise.resolve({ + id: '1234', + name, + image: Buffer.from(name), + mimetype: 'image/jpeg', + }); }, }; diff --git a/api/src/user/user.controller.ts b/api/src/user/user.controller.ts index 5ec15a2..195e327 100644 --- a/api/src/user/user.controller.ts +++ b/api/src/user/user.controller.ts @@ -29,6 +29,7 @@ import { QueryDto } from './dto/query.dto'; import { RemoveFieldsInterceptor } from './interceptor/remove-fields.interceptor'; import { FriendStatus } from './user.service'; import { ImageService } from '../image/image.service'; +import { AVATAR_MAX_SIZE } from 'src/utils'; declare global { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -108,14 +109,14 @@ export class UserController { } @UseInterceptors(RemoveFieldsInterceptor) - @HttpCode(HttpStatus.NO_CONTENT) + @HttpCode(HttpStatus.CREATED) @Post('/avatar') @UseInterceptors(FileInterceptor('image')) async createAvatar( @UploadedFile( new ParseFilePipe({ validators: [ - new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }), + new MaxFileSizeValidator({ maxSize: AVATAR_MAX_SIZE }), new FileTypeValidator({ fileType: '.(png|jpeg|gif)' }), ], }), @@ -125,27 +126,18 @@ export class UserController { ) { if (!req.user) throw new UnauthorizedException('user not found'); - const extension = file.originalname - .split('.') - .slice(-1) as unknown as string; const { username } = req.user; - const avatarName = `${username}.${extension}`; - - if (req.user.avatarName) { - this.imageService.deleteImage(req.user.avatarName); - } - - this.userService.updateUser(req.user.id, { avatarName }); + const { mimetype } = file; - return this.imageService.createImage(avatarName, file?.buffer); + return this.imageService.createImage(username, mimetype, file?.buffer); } @UseInterceptors(RemoveFieldsInterceptor) @UseGuards(AdminGuard) @HttpCode(HttpStatus.NO_CONTENT) @Delete('/avatar') - deleteAvatar(@Req() req: Request) { + async deleteAvatar(@Req() req: Request) { if (!req.user) throw new UnauthorizedException('user not found'); - return this.imageService.deleteImage(req.user?.username); + await this.imageService.deleteImage(req.user?.username); } } diff --git a/api/src/utils/constants.ts b/api/src/utils/constants.ts index 237789c..8f13a54 100644 --- a/api/src/utils/constants.ts +++ b/api/src/utils/constants.ts @@ -1,2 +1,3 @@ export const PER_PAGE = 500; export const PAGE_NUMBER = 0; +export const AVATAR_MAX_SIZE = 5 * 1024 * 1024; diff --git a/web/src/ambient.d.ts b/web/src/ambient.d.ts index da8dedf..0b676a0 100644 --- a/web/src/ambient.d.ts +++ b/web/src/ambient.d.ts @@ -59,3 +59,11 @@ export type ToastType = { dismissible: boolean; timeout: number; }; + +export type Stats = { + id?: string; + listCount: number; + moviesCount: number; + sharedListCount: number; + commentsCount: number; +}; diff --git a/web/src/lib/Avatar.svelte b/web/src/lib/Avatar.svelte index 11a8dbb..8a82625 100644 --- a/web/src/lib/Avatar.svelte +++ b/web/src/lib/Avatar.svelte @@ -1,6 +1,7 @@ + +