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
|
# 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
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:
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
|
@ -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
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"
|
"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"
|
||||||
|
|
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
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,
|
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()
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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