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
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
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:
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

View file

@ -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

View file

@ -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>

View file

@ -3,6 +3,8 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"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"
},
"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"

View file

@ -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: [
{

View file

@ -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
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,
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()

View file

@ -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 {}

View file

@ -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,
},
});
}
}

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