mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 09:31:16 +00:00
feat: added create kweek route & multi file upload
This commit is contained in:
parent
ac9026aea6
commit
cab4a0d9f1
21 changed files with 225 additions and 103 deletions
|
@ -3,6 +3,9 @@
|
|||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist/**", "node_modules"]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Kweek" ADD COLUMN "attachments" TEXT[];
|
|
@ -33,6 +33,7 @@ model Kweek {
|
|||
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||
likes KweekLike[]
|
||||
comments Comments[]
|
||||
attachments String[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { Public } from "src/public.decorator";
|
||||
import { Public } from "src/decorators/public.decorator";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { LoginUserDTO } from "./dto/login.dto";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ExecutionContext, Injectable } from "@nestjs/common";
|
|||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Observable } from "rxjs";
|
||||
import { IS_PUBLIC_KEY } from "src/public.decorator";
|
||||
import { IS_PUBLIC_KEY } from "src/decorators/public.decorator";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||
|
|
27
src/decorators/create-kweek.decorator.ts
Normal file
27
src/decorators/create-kweek.decorator.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Thanks sandeepsuvit @ https://github.com/nestjs/swagger/issues/417
|
||||
|
||||
import { ApiBody } from "@nestjs/swagger";
|
||||
|
||||
export const ApiCreateKweek =
|
||||
(fieldName: string): MethodDecorator =>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: idk typing for target
|
||||
(target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
ApiBody({
|
||||
required: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
content: {
|
||||
type: "string",
|
||||
},
|
||||
[fieldName]: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})(target, propertyKey, descriptor);
|
||||
};
|
|
@ -1,11 +0,0 @@
|
|||
import { createZodDto } from "nestjs-zod";
|
||||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const CreateKweekSchema = z
|
||||
.object({
|
||||
content: z.string({ required_error: "Kweek content is required" }).max(300),
|
||||
files: z.array(z.object({})),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class CreateKweekDTO extends createZodDto(CreateKweekSchema) {}
|
|
@ -1,4 +1,4 @@
|
|||
import { FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||
import { File, FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -17,8 +17,9 @@ import {
|
|||
ApiOperation,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { Public } from "src/public.decorator";
|
||||
import { CreateKweekDTO } from "./dto/create-kweek.dto";
|
||||
import { ApiCreateKweek } from "src/decorators/create-kweek.decorator";
|
||||
import { Public } from "src/decorators/public.decorator";
|
||||
import { MultiFileValidation } from "src/validators/multi-file.validator";
|
||||
import { UpdateKweekDTO } from "./dto/update-kweek.dto";
|
||||
import { KweeksService } from "./kweeks.service";
|
||||
|
||||
|
@ -28,17 +29,17 @@ export class KweeksController {
|
|||
constructor(private readonly kweeksService: KweeksService) {}
|
||||
|
||||
@Post()
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiCreateKweek("attachments")
|
||||
@UseInterceptors(FilesInterceptor("attachments", 4))
|
||||
@ApiOperation({ summary: "Creates a kweek" })
|
||||
@ApiBearerAuth("JWT")
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@UseInterceptors(FilesInterceptor("attachments", 4))
|
||||
create(
|
||||
@Body() createKweekDto: CreateKweekDTO,
|
||||
@UploadedFiles() attachments: File,
|
||||
@UploadedFiles(new MultiFileValidation()) attachments: Array<File>,
|
||||
@Body() body,
|
||||
@Request() req,
|
||||
) {
|
||||
// TODO: Find a way to handle multiple files with Swagger
|
||||
return this.kweeksService.create(createKweekDto);
|
||||
return this.kweeksService.create(body.content, req.user.id, attachments);
|
||||
}
|
||||
|
||||
@Public()
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { PrismaModule } from "src/services/prisma/prisma.module";
|
||||
import { S3Service } from "src/services/s3/s3.service";
|
||||
import { KweeksController } from "./kweeks.controller";
|
||||
import { KweeksService } from "./kweeks.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [KweeksController],
|
||||
providers: [KweeksService],
|
||||
providers: [KweeksService, S3Service],
|
||||
})
|
||||
export class KweeksModule {}
|
||||
|
|
|
@ -1,13 +1,35 @@
|
|||
import { File } from "@nest-lab/fastify-multer";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { CreateKweekDTO } from "./dto/create-kweek.dto";
|
||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||
import { S3Service } from "src/services/s3/s3.service";
|
||||
import { UpdateKweekDTO } from "./dto/update-kweek.dto";
|
||||
|
||||
@Injectable()
|
||||
export class KweeksService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
create(createKweekDto: CreateKweekDTO) {
|
||||
return "This action adds a new kweek";
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly s3: S3Service,
|
||||
) {}
|
||||
async create(content: string, authorId: string, files: Array<File>) {
|
||||
const post = await this.prisma.kweek.create({
|
||||
data: {
|
||||
content,
|
||||
authorId,
|
||||
}
|
||||
});
|
||||
|
||||
const attachments = await this.s3.multiImageUploadToMinio(post.id, files);
|
||||
|
||||
await this.prisma.kweek.updateMany({
|
||||
where: {
|
||||
id: post.id
|
||||
},
|
||||
data: {
|
||||
attachments
|
||||
},
|
||||
});
|
||||
|
||||
return await this.prisma.kweek.findUnique({where: {id: post.id}});
|
||||
}
|
||||
|
||||
findOne(id: number) {
|
||||
|
|
74
src/services/s3/s3.service.ts
Normal file
74
src/services/s3/s3.service.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { PutObjectCommand, PutObjectCommandInput } from "@aws-sdk/client-s3";
|
||||
import { BadRequestException, Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { InjectS3, S3 } from "nestjs-s3";
|
||||
import { File } from "@nest-lab/fastify-multer";
|
||||
import sharp from "sharp";
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
constructor(@InjectS3() private readonly s3: S3) {}
|
||||
|
||||
/**
|
||||
* Returns the image url if the upload to minio was successful.
|
||||
*/
|
||||
async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> {
|
||||
const compressedBuffer = await sharp(buffer)
|
||||
.resize(200, 200)
|
||||
.webp({ quality: 70 })
|
||||
.toBuffer();
|
||||
|
||||
const params: PutObjectCommandInput = {
|
||||
Bucket: process.env.MINIO_DEFAULT_BUCKETS,
|
||||
Key: `profile_images/${userID}.webp`,
|
||||
Body: compressedBuffer,
|
||||
ContentType: "image/webp",
|
||||
ContentDisposition: "inline",
|
||||
ACL: "public-read",
|
||||
};
|
||||
|
||||
const { ETag } = await this.s3.send(new PutObjectCommand(params));
|
||||
|
||||
if (ETag !== null) {
|
||||
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to upload the profile image",
|
||||
);
|
||||
}
|
||||
|
||||
async multiImageUploadToMinio(id: string, files: Array<File>) {
|
||||
const buffers: Buffer[] = [];
|
||||
|
||||
if (files.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const { buffer } = files[i];
|
||||
|
||||
const compressedBuffers = await sharp(buffer).webp({quality: 70}).toBuffer();
|
||||
buffers.push(compressedBuffers);
|
||||
}
|
||||
|
||||
const uploadPromises = buffers.map(async (buffer, index) => {
|
||||
return await this.multiUploadToMinio(buffer, id, index + 1);
|
||||
});
|
||||
|
||||
return Promise.all(uploadPromises);
|
||||
}
|
||||
|
||||
private async multiUploadToMinio(buffer: Buffer, id: string, index: number) {
|
||||
const Key = `posts/${id}/${index}.webp`;
|
||||
|
||||
const params: PutObjectCommandInput = {
|
||||
Bucket: process.env.MINIO_DEFAULT_BUCKETS,
|
||||
Key,
|
||||
Body: buffer,
|
||||
ContentType: 'image/webp',
|
||||
};
|
||||
|
||||
await this.s3.send(new PutObjectCommand(params))
|
||||
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/${Key}`
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import { PutObjectCommand, PutObjectCommandInput } from "@aws-sdk/client-s3";
|
||||
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { InjectS3, S3 } from "nestjs-s3";
|
||||
import sharp from "sharp";
|
||||
|
||||
@Injectable()
|
||||
export class S3Service {
|
||||
constructor(@InjectS3() private readonly s3: S3) {}
|
||||
|
||||
/**
|
||||
* Returns the image url if the upload to minio was successful.
|
||||
*/
|
||||
async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> {
|
||||
const compressedBuffer = await sharp(buffer)
|
||||
.resize(200, 200)
|
||||
.webp({ quality: 70 })
|
||||
.toBuffer();
|
||||
|
||||
const uploadParams: PutObjectCommandInput = {
|
||||
Bucket: process.env.MINIO_DEFAULT_BUCKETS,
|
||||
Key: `profile_images/${userID}.webp`,
|
||||
Body: compressedBuffer,
|
||||
ContentType: "image/webp",
|
||||
ContentDisposition: "inline",
|
||||
ACL: "public-read",
|
||||
};
|
||||
|
||||
const { ETag } = await this.s3.send(new PutObjectCommand(uploadParams));
|
||||
|
||||
if (ETag !== null) {
|
||||
return `${process.env.MINIO_ENDPOINT}/${process.env.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(
|
||||
"Failed to upload the profile image",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -23,8 +23,8 @@ import {
|
|||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { Public } from "src/public.decorator";
|
||||
import { BufferValidator } from "src/validators/buffer-validator.pipe";
|
||||
import { Public } from "src/decorators/public.decorator";
|
||||
import { BufferValidator } from "src/validators/buffer.validator";
|
||||
import UploadImageValidator from "src/validators/upload-image.validator";
|
||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||
import { UpdateEmailDTO } from "./dto/update-email.dto";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { S3Service } from "./s3.service";
|
||||
import { PrismaModule } from "src/services/prisma/prisma.module";
|
||||
import { S3Service } from "src/services/s3/s3.service";
|
||||
import { UserController } from "./users.controller";
|
||||
import { UserService } from "./users.service";
|
||||
|
||||
|
|
|
@ -5,10 +5,10 @@ import {
|
|||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||
import { S3Service } from "src/services/s3/s3.service";
|
||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
||||
import { UserModel } from "./models/user.model";
|
||||
import { S3Service } from "./s3.service";
|
||||
import { User } from "./types/user.type";
|
||||
|
||||
@Injectable()
|
||||
|
@ -53,6 +53,7 @@ export class UserService {
|
|||
select: {
|
||||
id: true,
|
||||
content: true,
|
||||
attachments: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
|
@ -106,10 +107,7 @@ export class UserService {
|
|||
return user;
|
||||
}
|
||||
|
||||
async updateEmail(
|
||||
id: string,
|
||||
email: string,
|
||||
): Promise<{ message: string }> {
|
||||
async updateEmail(id: string, email: string): Promise<{ message: string }> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id },
|
||||
});
|
||||
|
@ -155,7 +153,7 @@ export class UserService {
|
|||
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
id
|
||||
id,
|
||||
},
|
||||
data: {
|
||||
displayName,
|
||||
|
@ -199,10 +197,7 @@ export class UserService {
|
|||
}
|
||||
|
||||
async uploadImage(id: string, image: File) {
|
||||
const url = await this.s3.uploadImageToMinio(
|
||||
id,
|
||||
image.buffer,
|
||||
);
|
||||
const url = await this.s3.uploadImageToMinio(id, image.buffer);
|
||||
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
|
@ -220,10 +215,10 @@ export class UserService {
|
|||
async delete(id: string) {
|
||||
// TODO: Add validation for safety (like e-mail confirmation or password)
|
||||
try {
|
||||
await this.prisma.user.deleteMany({where: {id}});;
|
||||
return { message: "User deleted"}
|
||||
await this.prisma.user.deleteMany({ where: { id } });
|
||||
return { message: "User deleted" };
|
||||
} catch (e) {
|
||||
throw new BadRequestException('Error while trying to delete user')
|
||||
throw new BadRequestException("Error while trying to delete user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,9 @@ export class BufferValidator implements PipeTransform {
|
|||
) as Promise<typeof import("file-type")>);
|
||||
|
||||
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
||||
const Type = await fileTypeFromBuffer(value.buffer);
|
||||
const buffer_type = await fileTypeFromBuffer(value.buffer);
|
||||
|
||||
if (!Type || !ALLOWED_MIMES.includes(Type.mime)) {
|
||||
if (!buffer_type || !ALLOWED_MIMES.includes(buffer_type.mime)) {
|
||||
throw new BadRequestException(
|
||||
"Invalid file type. Should be jpeg, png or webp",
|
||||
);
|
45
src/validators/multi-file.validator.ts
Normal file
45
src/validators/multi-file.validator.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { File } from "@nest-lab/fastify-multer";
|
||||
import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class MultiFileValidation implements PipeTransform {
|
||||
async transform(value: Array<File>) {
|
||||
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
||||
|
||||
const errors = [];
|
||||
|
||||
const { fileTypeFromBuffer } = await (eval(
|
||||
'import("file-type")',
|
||||
) as Promise<typeof import("file-type")>);
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const file = value[i];
|
||||
const { buffer, size } = file;
|
||||
|
||||
try {
|
||||
const buffer_type = await fileTypeFromBuffer(buffer);
|
||||
|
||||
if (!buffer_type || !ALLOWED_MIMES.includes(buffer_type.mime)) {
|
||||
errors.push({
|
||||
file: i + 1,
|
||||
error: "Invalid file type. Should be jpeg, png or webp",
|
||||
});
|
||||
}
|
||||
|
||||
const maxFileSize = 1024 * 1024;
|
||||
|
||||
if (size > maxFileSize) {
|
||||
errors.push({ file: i + 1, error: "File is too big. Max 1MB" });
|
||||
}
|
||||
} catch (e) {
|
||||
errors.push({ file: i + 1, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length === 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new BadRequestException(errors);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import {
|
|||
const UploadImageValidator = new ParseFilePipe({
|
||||
validators: [
|
||||
new MaxFileSizeValidator({
|
||||
maxSize: 15 * 1024 * 1024,
|
||||
maxSize: 1024 * 1024,
|
||||
message: "File too big. Max 1MB.",
|
||||
}),
|
||||
new FileTypeValidator({ fileType: /^image\/(jpeg|png|webp)$/ }), // File extension validation
|
||||
|
|
Loading…
Reference in a new issue