mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 17:41:15 +00:00
feat: added swc, file upload, new route and minio
This commit is contained in:
parent
8af84b7cfa
commit
491cdd16aa
15 changed files with 3431 additions and 44 deletions
11
.env.example
11
.env.example
|
@ -25,9 +25,8 @@ CLIENT_URL=<placeholder>
|
|||
# Security
|
||||
JWT_ACCESS_SECRET=<placeholder>
|
||||
|
||||
# Localstack - The data can be fake (Change this to real data on production)
|
||||
SERVICES=s3
|
||||
AWS_ACCESS_KEY_ID=<placeholder>
|
||||
AWS_SECRET_ACCESS_KEY=<placeholder>
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_DEFAULT_OUTPUT=json
|
||||
# Minio
|
||||
MINIO_ROOT_USER=<username>
|
||||
MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
|
||||
MINIO_DEFAULT_BUCKETS=<bucket_name>
|
||||
MINIO_ENDPOINT=<url>
|
14
.swcrc
Normal file
14
.swcrc
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"minify": false
|
||||
}
|
|
@ -4,6 +4,9 @@ networks:
|
|||
localstack-net:
|
||||
name: localstack-net
|
||||
driver: bridge
|
||||
minio_network:
|
||||
name: minio_network
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
@ -27,24 +30,23 @@ services:
|
|||
volumes:
|
||||
- redis:/data
|
||||
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
container_name: localstack_main
|
||||
minio:
|
||||
image: bitnami/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- localstack-net
|
||||
ports:
|
||||
- 4566:4566
|
||||
- 4572:4572
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
networks:
|
||||
- minio_network
|
||||
volumes:
|
||||
- 'minio_data:/data'
|
||||
env_file:
|
||||
- docker.env
|
||||
volumes:
|
||||
- localstack:/data
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
name: backend-db
|
||||
redis:
|
||||
driver: local
|
||||
localstack:
|
||||
minio_data:
|
||||
driver: local
|
||||
|
|
|
@ -4,6 +4,9 @@ networks:
|
|||
localstack-net:
|
||||
name: localstack-net
|
||||
driver: bridge
|
||||
minio_network:
|
||||
name: minio_network
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
api:
|
||||
|
@ -32,20 +35,18 @@ services:
|
|||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
container_name: localstack_main
|
||||
minio:
|
||||
image: bitnami/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- localstack-net
|
||||
ports:
|
||||
- 4566:4566
|
||||
- 4572:4572
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
networks:
|
||||
- minio_network
|
||||
volumes:
|
||||
- 'minio_data:/data'
|
||||
env_file:
|
||||
- docker.env
|
||||
volumes:
|
||||
- localstack:/data
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
|
@ -62,4 +63,5 @@ volumes:
|
|||
name: backend-db
|
||||
redis:
|
||||
driver: local
|
||||
localstack:
|
||||
minio_data:
|
||||
driver: local
|
||||
|
|
|
@ -21,10 +21,8 @@ SERVER_PORT=<placeholder>
|
|||
# Security
|
||||
JWT_ACCESS_SECRET=<placeholder>
|
||||
|
||||
# Localstack - The data can be fake
|
||||
SERVICES=s3
|
||||
AWS_ACCESS_KEY_ID=<placeholder>
|
||||
AWS_SECRET_ACCESS_KEY=<placeholder>
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_DEFAULT_OUTPUT=json
|
||||
AWS_BUCKET=<placeholder>
|
||||
# Minio
|
||||
MINIO_ROOT_USER=<username>
|
||||
MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
|
||||
MINIO_DEFAULT_BUCKETS=<bucket_name>
|
||||
MINIO_ENDPOINT=<url>
|
|
@ -3,6 +3,8 @@
|
|||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"builder": "swc",
|
||||
"typeCheck": true
|
||||
}
|
||||
}
|
||||
|
|
3215
package-lock.json
generated
3215
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
@ -28,8 +28,11 @@
|
|||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.502.0",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/multipart": "^8.1.0",
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@nest-lab/fastify-multer": "^1.2.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
|
@ -41,20 +44,26 @@
|
|||
"@nestjs/throttler": "^5.1.1",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"file-type": "^19.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"nestjs-s3": "^2.0.1",
|
||||
"nestjs-throttler-storage-redis": "^0.4.1",
|
||||
"nestjs-zod": "^3.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1"
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.5.3",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@swc/cli": "^0.1.65",
|
||||
"@swc/core": "^1.3.107",
|
||||
"@swc/jest": "^0.2.31",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
|
@ -87,7 +96,9 @@
|
|||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
"^.+\\.(t|j)s$": [
|
||||
"@swc/jest"
|
||||
]
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
|
|
|
@ -7,7 +7,9 @@ import { ConfigModule } from "@nestjs/config";
|
|||
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
|
||||
import { KweeksModule } from './kweeks/kweeks.module';
|
||||
import { KweeksModule } from "./kweeks/kweeks.module";
|
||||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||
import { S3Module } from "nestjs-s3";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
@ -23,6 +25,18 @@ import { KweeksModule } from './kweeks/kweeks.module';
|
|||
),
|
||||
}),
|
||||
KweeksModule,
|
||||
FastifyMulterModule,
|
||||
S3Module.forRoot({
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3
|
||||
secretAccessKey: process.env.MINIO_ROOT_PASSWORD,
|
||||
},
|
||||
region: "us-east-1",
|
||||
endpoint: process.env.MINIO_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
|
@ -8,6 +8,8 @@ import {
|
|||
} from "@nestjs/platform-fastify";
|
||||
import * as helmet from "@fastify/helmet";
|
||||
|
||||
// TODO: File Upload (Posts and User Profile Image)
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
|
|
38
src/users/s3.service.ts
Normal file
38
src/users/s3.service.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
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 as successful. Otherwise returns `false`.
|
||||
*/
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
|
@ -2,16 +2,23 @@ import {
|
|||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
FileTypeValidator,
|
||||
Get,
|
||||
HttpCode,
|
||||
MaxFileSizeValidator,
|
||||
Param,
|
||||
ParseFilePipe,
|
||||
Patch,
|
||||
Post,
|
||||
Request,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
ApiCreatedResponse,
|
||||
ApiNotFoundResponse,
|
||||
ApiOperation,
|
||||
|
@ -25,6 +32,8 @@ import { UpdateNameDTO } from "./dto/update-name.dto";
|
|||
import { User } from "./types/user.type";
|
||||
import { UpdateEmailDTO } from "./dto/update-email.dto";
|
||||
import { UpdatePasswordDTO } from "./dto/update-password.dto";
|
||||
import { File, FileInterceptor } from "@nest-lab/fastify-multer";
|
||||
import { BufferValidator } from "src/validators/buffer-validator.pipe";
|
||||
|
||||
@ApiTags("Users")
|
||||
@Controller("users")
|
||||
|
@ -95,9 +104,42 @@ export class UserController {
|
|||
}
|
||||
|
||||
@Patch("/image")
|
||||
@ApiOperation({ summary: "Add a profile image" })
|
||||
@ApiOperation({
|
||||
summary: "Add a profile image",
|
||||
})
|
||||
@ApiBearerAuth("JWT")
|
||||
uploadProfileImage() {}
|
||||
@UseInterceptors(FileInterceptor("image"))
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiBody({
|
||||
required: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
image: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
uploadProfileImage(
|
||||
@UploadedFile(
|
||||
new ParseFilePipe({
|
||||
validators: [
|
||||
new MaxFileSizeValidator({
|
||||
maxSize: 15 * 1024 * 1024,
|
||||
message: "File too big. Max 1MB.",
|
||||
}),
|
||||
new FileTypeValidator({ fileType: /^image\/(jpeg|png|webp)$/ }), // File extension validation
|
||||
],
|
||||
}),
|
||||
new BufferValidator(), // Magic number validation
|
||||
)
|
||||
image: File,
|
||||
@Request() req,
|
||||
) {
|
||||
return this.userService.uploadImage(req.user, image);
|
||||
}
|
||||
|
||||
// DELETE
|
||||
@Delete()
|
||||
|
|
|
@ -2,11 +2,12 @@ import { Module } from "@nestjs/common";
|
|||
import { UserController } from "./users.controller";
|
||||
import { UserService } from "./users.service";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { S3Service } from "./s3.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
providers: [UserService, S3Service],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
||||
|
|
|
@ -8,10 +8,15 @@ import { PrismaService } from "src/prisma/prisma.service";
|
|||
import { UserModel } from "./models/user.model";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { User } from "./types/user.type";
|
||||
import { File } from "@nest-lab/fastify-multer";
|
||||
import { S3Service } from "./s3.service";
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private s3: S3Service,
|
||||
) {}
|
||||
async auth_search(username: string): Promise<UserModel> {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
|
@ -194,4 +199,23 @@ export class UserService {
|
|||
|
||||
return { message: "Password updated successfully" };
|
||||
}
|
||||
|
||||
async uploadImage(authenticatedUser: User, image: File) {
|
||||
const url = await this.s3.uploadImageToMinio(
|
||||
authenticatedUser.id,
|
||||
image.buffer,
|
||||
);
|
||||
|
||||
return await this.prisma.user.update({
|
||||
where: {
|
||||
id: authenticatedUser.id,
|
||||
},
|
||||
data: {
|
||||
profileImage: url,
|
||||
},
|
||||
select: {
|
||||
profileImage: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
25
src/validators/buffer-validator.pipe.ts
Normal file
25
src/validators/buffer-validator.pipe.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { File } from "@nest-lab/fastify-multer";
|
||||
import { BadRequestException, Injectable, PipeTransform } from "@nestjs/common";
|
||||
|
||||
/**
|
||||
* Magic Number validation with `file-type` module.
|
||||
*/
|
||||
@Injectable()
|
||||
export class BufferValidator implements PipeTransform {
|
||||
async transform(value: File) {
|
||||
const { fileTypeFromBuffer } = await (eval(
|
||||
'import("file-type")',
|
||||
) as Promise<typeof import("file-type")>);
|
||||
|
||||
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
||||
const Type = await fileTypeFromBuffer(value.buffer);
|
||||
|
||||
if (!Type || !ALLOWED_MIMES.includes(Type.mime)) {
|
||||
throw new BadRequestException(
|
||||
"Invalid file type. Should be jpeg, png or webp",
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue