feat: added swc, file upload, new route and minio

This commit is contained in:
Hackntosh 2024-01-30 20:22:17 +00:00
parent 8af84b7cfa
commit 491cdd16aa
15 changed files with 3431 additions and 44 deletions

View file

@ -25,9 +25,8 @@ CLIENT_URL=<placeholder>
# Security # Security
JWT_ACCESS_SECRET=<placeholder> JWT_ACCESS_SECRET=<placeholder>
# Localstack - The data can be fake (Change this to real data on production) # Minio
SERVICES=s3 MINIO_ROOT_USER=<username>
AWS_ACCESS_KEY_ID=<placeholder> MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
AWS_SECRET_ACCESS_KEY=<placeholder> MINIO_DEFAULT_BUCKETS=<bucket_name>
AWS_DEFAULT_REGION=us-east-1 MINIO_ENDPOINT=<url>
AWS_DEFAULT_OUTPUT=json

14
.swcrc Normal file
View 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
}

View file

@ -4,6 +4,9 @@ networks:
localstack-net: localstack-net:
name: localstack-net name: localstack-net
driver: bridge driver: bridge
minio_network:
name: minio_network
driver: bridge
services: services:
postgres: postgres:
@ -27,24 +30,23 @@ services:
volumes: volumes:
- redis:/data - redis:/data
localstack: minio:
image: localstack/localstack image: bitnami/minio
container_name: localstack_main
restart: unless-stopped restart: unless-stopped
networks:
- localstack-net
ports: ports:
- 4566:4566 - '9000:9000'
- 4572:4572 - '9001:9001'
networks:
- minio_network
volumes:
- 'minio_data:/data'
env_file: env_file:
- docker.env - docker.env
volumes:
- localstack:/data
- '/var/run/docker.sock:/var/run/docker.sock'
volumes: volumes:
postgres: postgres:
name: backend-db name: backend-db
redis: redis:
driver: local driver: local
localstack: minio_data:
driver: local

View file

@ -4,6 +4,9 @@ networks:
localstack-net: localstack-net:
name: localstack-net name: localstack-net
driver: bridge driver: bridge
minio_network:
name: minio_network
driver: bridge
services: services:
api: api:
@ -32,20 +35,18 @@ services:
volumes: volumes:
- postgres:/var/lib/postgresql/data - postgres:/var/lib/postgresql/data
localstack: minio:
image: localstack/localstack image: bitnami/minio
container_name: localstack_main
restart: unless-stopped restart: unless-stopped
networks:
- localstack-net
ports: ports:
- 4566:4566 - '9000:9000'
- 4572:4572 - '9001:9001'
networks:
- minio_network
volumes:
- 'minio_data:/data'
env_file: env_file:
- docker.env - docker.env
volumes:
- localstack:/data
- '/var/run/docker.sock:/var/run/docker.sock'
redis: redis:
image: redis:alpine image: redis:alpine
@ -62,4 +63,5 @@ volumes:
name: backend-db name: backend-db
redis: redis:
driver: local driver: local
localstack: minio_data:
driver: local

View file

@ -21,10 +21,8 @@ SERVER_PORT=<placeholder>
# Security # Security
JWT_ACCESS_SECRET=<placeholder> JWT_ACCESS_SECRET=<placeholder>
# Localstack - The data can be fake # Minio
SERVICES=s3 MINIO_ROOT_USER=<username>
AWS_ACCESS_KEY_ID=<placeholder> MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
AWS_SECRET_ACCESS_KEY=<placeholder> MINIO_DEFAULT_BUCKETS=<bucket_name>
AWS_DEFAULT_REGION=us-east-1 MINIO_ENDPOINT=<url>
AWS_DEFAULT_OUTPUT=json
AWS_BUCKET=<placeholder>

View file

@ -3,6 +3,8 @@
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true "deleteOutDir": true,
"builder": "swc",
"typeCheck": true
} }
} }

3215
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -28,8 +28,11 @@
"test:watch": "jest --watch" "test:watch": "jest --watch"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.502.0",
"@fastify/helmet": "^11.1.1", "@fastify/helmet": "^11.1.1",
"@fastify/multipart": "^8.1.0",
"@fastify/static": "^6.12.0", "@fastify/static": "^6.12.0",
"@nest-lab/fastify-multer": "^1.2.0",
"@nestjs/common": "^10.0.0", "@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1", "@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0", "@nestjs/core": "^10.0.0",
@ -41,20 +44,26 @@
"@nestjs/throttler": "^5.1.1", "@nestjs/throttler": "^5.1.1",
"@prisma/client": "^5.8.1", "@prisma/client": "^5.8.1",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"file-type": "^19.0.0",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"nestjs-s3": "^2.0.1",
"nestjs-throttler-storage-redis": "^0.4.1", "nestjs-throttler-storage-redis": "^0.4.1",
"nestjs-zod": "^3.0.0", "nestjs-zod": "^3.0.0",
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1" "rxjs": "^7.8.1",
"sharp": "^0.33.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.5.3", "@biomejs/biome": "1.5.3",
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^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/bcrypt": "^5.0.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
@ -87,7 +96,9 @@
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": [
"@swc/jest"
]
}, },
"collectCoverageFrom": [ "collectCoverageFrom": [
"**/*.(t|j)s" "**/*.(t|j)s"

View file

@ -7,7 +7,9 @@ import { ConfigModule } from "@nestjs/config";
import { JwtAuthGuard } from "./auth/jwt-auth.guard"; import { JwtAuthGuard } from "./auth/jwt-auth.guard";
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis"; 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({ @Module({
imports: [ imports: [
@ -23,6 +25,18 @@ import { KweeksModule } from './kweeks/kweeks.module';
), ),
}), }),
KweeksModule, 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: [ providers: [
{ {

View file

@ -8,6 +8,8 @@ import {
} from "@nestjs/platform-fastify"; } from "@nestjs/platform-fastify";
import * as helmet from "@fastify/helmet"; import * as helmet from "@fastify/helmet";
// TODO: File Upload (Posts and User Profile Image)
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
AppModule, AppModule,

38
src/users/s3.service.ts Normal file
View 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",
);
}
}

View file

@ -2,16 +2,23 @@ import {
Body, Body,
Controller, Controller,
Delete, Delete,
FileTypeValidator,
Get, Get,
HttpCode, HttpCode,
MaxFileSizeValidator,
Param, Param,
ParseFilePipe,
Patch, Patch,
Post, Post,
Request, Request,
UploadedFile,
UseInterceptors,
} from "@nestjs/common"; } from "@nestjs/common";
import { import {
ApiBadRequestResponse, ApiBadRequestResponse,
ApiBearerAuth, ApiBearerAuth,
ApiBody,
ApiConsumes,
ApiCreatedResponse, ApiCreatedResponse,
ApiNotFoundResponse, ApiNotFoundResponse,
ApiOperation, ApiOperation,
@ -25,6 +32,8 @@ import { UpdateNameDTO } from "./dto/update-name.dto";
import { User } from "./types/user.type"; import { User } from "./types/user.type";
import { UpdateEmailDTO } from "./dto/update-email.dto"; import { UpdateEmailDTO } from "./dto/update-email.dto";
import { UpdatePasswordDTO } from "./dto/update-password.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") @ApiTags("Users")
@Controller("users") @Controller("users")
@ -95,9 +104,42 @@ export class UserController {
} }
@Patch("/image") @Patch("/image")
@ApiOperation({ summary: "Add a profile image" }) @ApiOperation({
summary: "Add a profile image",
})
@ApiBearerAuth("JWT") @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
@Delete() @Delete()

View file

@ -2,11 +2,12 @@ import { Module } from "@nestjs/common";
import { UserController } from "./users.controller"; import { UserController } from "./users.controller";
import { UserService } from "./users.service"; import { UserService } from "./users.service";
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from "src/prisma/prisma.module";
import { S3Service } from "./s3.service";
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService], providers: [UserService, S3Service],
exports: [UserService], exports: [UserService],
}) })
export class UserModule {} export class UserModule {}

View file

@ -8,10 +8,15 @@ import { PrismaService } from "src/prisma/prisma.service";
import { UserModel } from "./models/user.model"; import { UserModel } from "./models/user.model";
import * as bcrypt from "bcrypt"; import * as bcrypt from "bcrypt";
import { User } from "./types/user.type"; import { User } from "./types/user.type";
import { File } from "@nest-lab/fastify-multer";
import { S3Service } from "./s3.service";
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor(private prisma: PrismaService) {} constructor(
private prisma: PrismaService,
private s3: S3Service,
) {}
async auth_search(username: string): Promise<UserModel> { async auth_search(username: string): Promise<UserModel> {
const user = await this.prisma.user.findFirst({ const user = await this.prisma.user.findFirst({
where: { where: {
@ -194,4 +199,23 @@ export class UserService {
return { message: "Password updated successfully" }; 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,
},
});
}
} }

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