From 9591dbdcec74116879bb3057579406e46c64e789 Mon Sep 17 00:00:00 2001 From: kangjuhyup Date: Sun, 27 Oct 2024 19:09:24 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EC=B4=88=EB=8C=80=EC=9E=A5=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes package.json | 7 + src/.DS_Store | Bin 0 -> 6148 bytes src/app.module.ts | 12 +- src/database/database.module.ts | 5 + src/database/repository/attachment.ts | 48 +++ src/database/repository/letter.ts | 47 +++ src/database/repository/param/attachment.ts | 16 + src/database/repository/param/default.ts | 5 + src/database/repository/param/letter.ts | 20 ++ src/database/transaction.base.ts | 46 +++ src/domain/auth/auth.controller.ts | 97 +++--- src/domain/auth/auth.module.ts | 67 ++-- src/domain/auth/auth.service.ts | 45 +-- src/domain/auth/dto/sign.ts | 16 +- src/domain/letter/dto/response/add.letter.ts | 7 + src/domain/letter/dto/response/prepare.ts | 19 +- src/domain/letter/letter.controller.ts | 115 ++++--- src/domain/letter/letter.service.ts | 136 ++++++-- .../letter/transaction/insert.letter.ts | 82 +++++ src/domain/user/user.module.ts | 14 +- src/domain/user/user.service.ts | 52 +-- src/jwt/guard/user.guard.ts | 60 ++-- src/jwt/strategy/user.strategy.ts | 12 +- src/jwt/user.ts | 4 +- src/redis/redis.client.service.ts | 48 +++ src/redis/redis.module.ts | 43 +++ src/storage/storage.service.ts | 39 +++ src/util/random.ts | 17 +- src/util/yn.ts | 4 + tsconfig.json | 2 + yarn.lock | 296 +++++++++++++++++- 32 files changed, 1111 insertions(+), 270 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store create mode 100644 src/database/repository/attachment.ts create mode 100644 src/database/repository/letter.ts create mode 100644 src/database/repository/param/attachment.ts create mode 100644 src/database/repository/param/default.ts create mode 100644 src/database/repository/param/letter.ts create mode 100644 src/database/transaction.base.ts create mode 100644 src/domain/letter/dto/response/add.letter.ts create mode 100644 src/domain/letter/transaction/insert.letter.ts create mode 100644 src/redis/redis.client.service.ts create mode 100644 src/redis/redis.module.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..aafb8f7611cc655cbaaa1f4796ef33b895a0d5f7 GIT binary patch literal 6148 zcmeHK%}T>S5Z-O8O({YS3Oz1(Em+%95HBIt7cim+m718M!I&*cY7V84v%Zi|;`2DO zy8(+ii`W_1{pNQ!`$6`HF~;3xc+8m17_*=ua#U&r-L;{HNk-&2Mz#oI8G!W>EKKaL z1AcpxWh`Y6LGk_j<0#7qgHPUQwsv;gR@>@YcixjMyxh;1nd_%FXkAJf2bJyzSJAwf z+52ZQ$^9sqr>Yo Pq>F$eggRp27Z~^g>O@I` literal 0 HcmV?d00001 diff --git a/package.json b/package.json index bb322dc..63620f5 100644 --- a/package.json +++ b/package.json @@ -24,13 +24,18 @@ "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.0.0", + "@nestjs/jwt": "^10.2.0", + "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", "@nestjs/swagger": "^7.4.2", "@nestjs/typeorm": "^10.0.2", "aws-sdk": "^2.1691.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "ioredis": "^5.4.1", "mysql2": "^3.11.3", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.1", @@ -45,6 +50,8 @@ "@types/jest": "^29.5.2", "@types/multer": "^1.4.12", "@types/node": "^20.3.1", + "@types/passport": "^0", + "@types/passport-jwt": "^4", "@types/supertest": "^6.0.0", "@types/uuid": "^10", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..368c281d834446703a05608cd4d002dc328f75bb GIT binary patch literal 6148 zcmeHKL2uJA6n<{IHKp2AB!+fDio|s)Wt%EZTuK=St^~mWP)XKk1k$)_()CbP>UsPJ z{sLEi3IBx?e9v~Lqz&2)D&$u^e~$g0^Ze!6u8BzWr^ClYEh2KDjI}!`N`(7aH>6@( z_JKl;aY2$2%4h<#Vi~XuY&!$IcJGrjI}w!x3v-WZblek7a7u2#ZmHZ@nFq!|Hd;_kzY}SsKc_j#g{c- z2XFE*Kc509{EPDxKX0=SX6e}Q@;w;GQJQr+KSZTk+u5xAvuGz?+#?oE^o<*YBK$uL#Ep6+lOPGitIoX?ND zog??;w7WQR=V!;LpPqCVi-vRX@X?c(gU|6Ok=K|5L}0I0vS)Aw%*h51SKcIvWpar= z_S*$dl1AVtqNK=Cg>CY=&}18M%2~wUVT`(@NzqGX(AQiBRp}GWC|k4QEXiBdS22`+ zUCxZW^$sEk5qY$pzbX&%mUVo46(wePS(*iT>l(5%!>DmFTE#@(4&iK}m^^TrTNs-S z$ueLW__qx3{@_9xdj^*p)z*PRT>*eKG%G=!{}!;1Yp`c+QloY!VJ;uSyjhqVicoLI_^vW1(bH&4%YbE|$Us#;w)p&i{^$FD(aAnp1}p>r z6$7Gn?w$8BC3CiJOpec57y1Fp!gfoI$_0fvj#Y(^;zOts^toIC_6#mHq6K3A2q+qC KVHx { return { host: config.get('REDIS_HOST'), @@ -41,8 +39,8 @@ const modules = [ password: config.get('REDIS_PWD'), }; }, - inject : [ConfigService] - }) + inject: [ConfigService], + }), ].filter(Boolean); @Module({ diff --git a/src/database/database.module.ts b/src/database/database.module.ts index fce4be4..c6b6d28 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -4,6 +4,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { LetterDataSource } from './datasource/letter.datasource'; import { Enviroments } from '@app/domain/dto/env'; import { plainToInstance } from 'class-transformer'; +import { LetterRepository } from './repository/letter'; + +const repositories = [LetterRepository]; @Global() @Module({ @@ -29,5 +32,7 @@ import { plainToInstance } from 'class-transformer'; inject: [ConfigService], }), ], + providers: [...repositories], + exports: [...repositories], }) export class DatabaseModule {} diff --git a/src/database/repository/attachment.ts b/src/database/repository/attachment.ts new file mode 100644 index 0000000..94983c9 --- /dev/null +++ b/src/database/repository/attachment.ts @@ -0,0 +1,48 @@ +import { AttachmentEntity } from '@database/entity/attachment'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, In, Repository } from 'typeorm'; +import { InsertAttachment, SelectAttachment } from './param/attachment'; + +export class AttachmentRepository { + constructor( + @InjectRepository(AttachmentEntity) + private readonly attachment: Repository, + ) {} + + async selectAttachmentIds({ + attachmentPaths, + entityManager, + }: Pick): Promise< + number[] + > { + const repo = this._getRepository('attachment', entityManager); + return ( + await repo + .createQueryBuilder() + .select(['attachmentId']) + .where({ + attachmentPath: In(attachmentPaths), + }) + .getMany() + ).map((att) => att.attachmentId); + } + + async bulkInsertAttachments({ + attachments, + entityManager, + }: Omit) { + const repo = this._getRepository('attachment', entityManager); + return await repo + .createQueryBuilder() + .insert() + .values(attachments) + .execute(); + } + + private _getRepository(type: 'attachment', entityManager?: EntityManager) { + if (type === 'attachment') + return entityManager + ? entityManager.getRepository(AttachmentEntity) + : this.attachment; + } +} diff --git a/src/database/repository/letter.ts b/src/database/repository/letter.ts new file mode 100644 index 0000000..22119e5 --- /dev/null +++ b/src/database/repository/letter.ts @@ -0,0 +1,47 @@ +import { LetterEntity } from '@database/entity/letter'; +import { LetterAttachmentEntity } from '@database/entity/letter.attachment'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityManager, Repository } from 'typeorm'; +import { InsertLetter, InsertLetterAttachment } from './param/letter'; + +@Injectable() +export class LetterRepository { + constructor( + @InjectRepository(LetterEntity) + private readonly letter: Repository, + @InjectRepository(LetterAttachmentEntity) + private readonly letterAttachment: Repository, + ) {} + + async insertLetter({ letter, entityManager }: InsertLetter) { + const repo = this._getRepository('letter', entityManager); + return await repo.insert(letter); + } + + async insertLetterAttachment({ + letterAttachments, + entityManager, + }: InsertLetterAttachment) { + const repo = this._getRepository('letterAttachment', entityManager); + await repo + .createQueryBuilder() + .insert() + .values(letterAttachments) + .execute(); + } + + private _getRepository( + type: 'letter' | 'letterAttachment', + entityManager?: EntityManager, + ) { + if (type === 'letter') + return entityManager + ? entityManager.getRepository(LetterEntity) + : this.letter; + if (type === 'letterAttachment') + return entityManager + ? entityManager.getRepository(LetterAttachmentEntity) + : this.letterAttachment; + } +} diff --git a/src/database/repository/param/attachment.ts b/src/database/repository/param/attachment.ts new file mode 100644 index 0000000..213a5a2 --- /dev/null +++ b/src/database/repository/param/attachment.ts @@ -0,0 +1,16 @@ +import { AttachmentEntity } from '@database/entity/attachment'; +import { DefaultParameter } from './default'; + +export type Attachment = Pick; + +export class SelectAttachment extends DefaultParameter { + attachmentId: number; + attachmentIds: number[]; + attachmentPath: string; + attachmentPaths: string[]; +} + +export class InsertAttachment extends DefaultParameter { + attachments: Array; + attachment: Attachment; +} diff --git a/src/database/repository/param/default.ts b/src/database/repository/param/default.ts new file mode 100644 index 0000000..802f972 --- /dev/null +++ b/src/database/repository/param/default.ts @@ -0,0 +1,5 @@ +import { EntityManager } from 'typeorm'; + +export class DefaultParameter { + entityManager: EntityManager; +} diff --git a/src/database/repository/param/letter.ts b/src/database/repository/param/letter.ts new file mode 100644 index 0000000..5e2cbac --- /dev/null +++ b/src/database/repository/param/letter.ts @@ -0,0 +1,20 @@ +import { LetterEntity } from '@database/entity/letter'; +import { DefaultParameter } from './default'; +import { LetterAttachmentEntity } from '@database/entity/letter.attachment'; + +export type Letter = Pick< + LetterEntity, + 'userId' | 'letterCategoryCode' | 'title' | 'body' | 'commentYn' | 'attendYn' +>; + +export type LetterAttachment = Pick< + LetterAttachmentEntity, + 'letterId' | 'attachmentId' +>; + +export class InsertLetter extends DefaultParameter { + letter: Letter; +} +export class InsertLetterAttachment extends DefaultParameter { + letterAttachments: Array; +} diff --git a/src/database/transaction.base.ts b/src/database/transaction.base.ts new file mode 100644 index 0000000..e1638df --- /dev/null +++ b/src/database/transaction.base.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@nestjs/common'; +import { DataSource, EntityManager, QueryRunner } from 'typeorm'; +/** + * 트랜잭션에는 데이터베이스 CRUD 로직과 비즈니스 로직이 모두 포함된다. + * 서비스 레이어와 데이터 레이어 사이에서 트랜잭션을 실행시키기 위한 추상 클래스 + * 이 클래스를 상속받아 필요한 트랜잭션을 작성한다. + */ + +@Injectable() +export abstract class BaseTransaction { + protected constructor(private readonly datasource: DataSource) {} + + protected abstract execute( + data: TransactionInput, + manager: EntityManager, + ): Promise; + + private async createRunner(): Promise { + return this.datasource.createQueryRunner(); + } + + async run(data: TransactionInput): Promise { + const queryRunner = await this.createRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction('REPEATABLE READ'); // 격리수준 RPEATABLE READ + + try { + const result = await this.execute(data, queryRunner.manager); + await queryRunner.commitTransaction(); + return result; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + async runWithinTransaction( + data: TransactionInput, + manager: EntityManager, + ): Promise { + return this.execute(data, manager); + } +} diff --git a/src/domain/auth/auth.controller.ts b/src/domain/auth/auth.controller.ts index ec0402d..02b9bba 100644 --- a/src/domain/auth/auth.controller.ts +++ b/src/domain/auth/auth.controller.ts @@ -1,55 +1,58 @@ -import { Body, Controller, Delete, Post, Req, Res, UseGuards } from "@nestjs/common"; -import { SignRequest } from "./dto/sign"; -import { AuthService } from "./auth.service"; -import { UserGuard } from "@app/jwt/guard/user.guard"; +import { + Body, + Controller, + Delete, + Post, + Req, + Res, + UseGuards, +} from '@nestjs/common'; +import { SignRequest } from './dto/sign'; +import { AuthService } from './auth.service'; +import { UserGuard } from '@app/jwt/guard/user.guard'; import { Response } from 'express'; @Controller('auth') export class AuthController { + constructor(private readonly authService: AuthService) {} - constructor( - private readonly authService : AuthService - ) {} + @Post('signUp') + async signUp( + @Body() dto: SignRequest, + @Res({ passthrough: true }) res: Response, + ) { + const data = await this.authService.signUp(dto.phone, dto.password); + res.setHeader('Authorization', `Bearer ${data.access}`); + return { + result: true, + }; + } - @Post('signUp') - async signUp( - @Body() dto : SignRequest, - @Res({passthrough : true}) res:Response - ) { - const data = await this.authService.signUp(dto.phone,dto.password); - res.setHeader('Authorization',`Bearer ${data.access}`) - return { - result : true - } - } + @Post('signIn') + async signIn( + @Body() dto: SignRequest, + @Res({ passthrough: true }) res: Response, + ) { + const data = await this.authService.signIn(dto.phone, dto.password); + res.setHeader('Authorization', `Bearer ${data.access}`); + return { + result: true, + }; + } - @Post('signIn') - async signIn( - @Body() dto : SignRequest, - @Res({passthrough : true}) res:Response - ) { - const data = await this.authService.signIn(dto.phone,dto.password); - res.setHeader('Authorization',`Bearer ${data.access}`) - return { - result : true - } - } + @Post('signOut') + @UseGuards(UserGuard) + async signOut(@Req() req) { + return { + result: true, + }; + } - @Post('signOut') - @UseGuards(UserGuard) - async signOut( - @Req() req - ) { - return { - result : true - } - } - - @Delete('account') - @UseGuards(UserGuard) - async deleteAccount(@Body() dto: SignRequest) { - return { - result : true - } - } -} \ No newline at end of file + @Delete('account') + @UseGuards(UserGuard) + async deleteAccount(@Body() dto: SignRequest) { + return { + result: true, + }; + } +} diff --git a/src/domain/auth/auth.module.ts b/src/domain/auth/auth.module.ts index 9d98542..bfb752d 100644 --- a/src/domain/auth/auth.module.ts +++ b/src/domain/auth/auth.module.ts @@ -1,35 +1,38 @@ -import { DynamicModule, Module } from "@nestjs/common"; -import { UserService } from "../user/user.service"; -import { PassportModule } from "@nestjs/passport"; -import { JwtModule } from "@nestjs/jwt"; -import { UserStrategy } from "./strategy/user.strategy"; -import { UserModule } from "../user/user.module"; -import { AuthController } from "./auth.controller"; +import { DynamicModule, Module } from '@nestjs/common'; +import { UserService } from '../user/user.service'; +import { PassportModule } from '@nestjs/passport'; +import { JwtModule } from '@nestjs/jwt'; +import { UserStrategy } from './strategy/user.strategy'; +import { UserModule } from '../user/user.module'; +import { AuthController } from './auth.controller'; @Module({}) export class AuthModule { - static forRootAsync(options : { secret : string, expiresIn : string }) : DynamicModule { - return { - module : AuthModule, - imports : [ - UserModule, - PassportModule, - JwtModule.register({ - secret : options.secret, - signOptions : { - expiresIn : options.expiresIn - } - }) - ], - controllers : [ - AuthController, - ], - providers : [ - { - provide : UserStrategy, - useFactory : (user:UserService) => new UserStrategy(user,options.secret), - inject : [UserService] - }], - } - } -} \ No newline at end of file + static forRootAsync(options: { + secret: string; + expiresIn: string; + }): DynamicModule { + return { + module: AuthModule, + imports: [ + UserModule, + PassportModule, + JwtModule.register({ + secret: options.secret, + signOptions: { + expiresIn: options.expiresIn, + }, + }), + ], + controllers: [AuthController], + providers: [ + { + provide: UserStrategy, + useFactory: (user: UserService) => + new UserStrategy(user, options.secret), + inject: [UserService], + }, + ], + }; + } +} diff --git a/src/domain/auth/auth.service.ts b/src/domain/auth/auth.service.ts index 679a648..67a70ed 100644 --- a/src/domain/auth/auth.service.ts +++ b/src/domain/auth/auth.service.ts @@ -1,29 +1,30 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; -import { JwtService } from "@nestjs/jwt"; -import { UserService } from "../user/user.service"; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { UserService } from '../user/user.service'; @Injectable() export class AuthService { - constructor( - private readonly userService: UserService, - private readonly jwtService: JwtService, - ) {} + constructor( + private readonly userService: UserService, + private readonly jwtService: JwtService, + ) {} - async signIn(phone: string, pwd: string) { - const user = await this.userService.getUser(phone); - if(user.pwd !== pwd) throw new UnauthorizedException('잘못된 비밀번호 입니다.') - return this._generateAccessToken(user.userId) - } + async signIn(phone: string, pwd: string) { + const user = await this.userService.getUser(phone); + if (user.pwd !== pwd) + throw new UnauthorizedException('잘못된 비밀번호 입니다.'); + return this._generateAccessToken(user.userId); + } - async signUp(phone: string, pwd:string) { - const user = await this.userService.saveUser(phone,pwd); - return this._generateAccessToken(user.userId); - } + async signUp(phone: string, pwd: string) { + const user = await this.userService.saveUser(phone, pwd); + return this._generateAccessToken(user.userId); + } - private _generateAccessToken(id:string) { - const payload = { id }; - return { - access: this.jwtService.sign(payload), - }; - } + private _generateAccessToken(id: string) { + const payload = { id }; + return { + access: this.jwtService.sign(payload), + }; + } } diff --git a/src/domain/auth/dto/sign.ts b/src/domain/auth/dto/sign.ts index 5169379..3fb59cd 100644 --- a/src/domain/auth/dto/sign.ts +++ b/src/domain/auth/dto/sign.ts @@ -1,11 +1,11 @@ -import { IsNotEmpty, IsOptional, IsPhoneNumber, IsString } from "class-validator" +import { IsNotEmpty, IsPhoneNumber, IsString } from 'class-validator'; export class SignRequest { - @IsNotEmpty() - @IsPhoneNumber() - phone : string + @IsNotEmpty() + @IsPhoneNumber() + phone: string; - @IsNotEmpty() - @IsString() - password : string -} \ No newline at end of file + @IsNotEmpty() + @IsString() + password: string; +} diff --git a/src/domain/letter/dto/response/add.letter.ts b/src/domain/letter/dto/response/add.letter.ts new file mode 100644 index 0000000..a2efd0c --- /dev/null +++ b/src/domain/letter/dto/response/add.letter.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsNumber } from 'class-validator'; + +export class AddLetterResponse { + @IsNotEmpty() + @IsNumber() + letterId: number; +} diff --git a/src/domain/letter/dto/response/prepare.ts b/src/domain/letter/dto/response/prepare.ts index 1ceecf2..2d8b944 100644 --- a/src/domain/letter/dto/response/prepare.ts +++ b/src/domain/letter/dto/response/prepare.ts @@ -4,6 +4,7 @@ import { IsNotEmpty, IsNumber, IsOptional, + IsString, IsUrl, } from 'class-validator'; @@ -17,6 +18,15 @@ export class PrepareResponse { @IsUrl() thumbnailUrl: string; + @ApiProperty({ + description: '초대장 업로드 권한이 열려있는 url', + example: + 'https://s3.ap-northeast-1.wasabisys.com/bucket/file.txt?AWSAccessKeyId=...', + }) + @IsNotEmpty() + @IsUrl() + letterUrl: string; + @ApiProperty({ description: '배경 업로드 권한이 열려있는 url', example: @@ -33,7 +43,7 @@ export class PrepareResponse { }) @IsOptional() @IsArray() - @IsUrl({},{each:true}) + @IsUrl({}, { each: true }) componentUrls: string[]; @ApiProperty({ @@ -43,4 +53,11 @@ export class PrepareResponse { @IsNotEmpty() @IsNumber() expires: number; + + @ApiProperty({ + description: '세션키', + }) + @IsNotEmpty() + @IsString() + sessionKey: string; } diff --git a/src/domain/letter/letter.controller.ts b/src/domain/letter/letter.controller.ts index 0f83405..13f5bfd 100644 --- a/src/domain/letter/letter.controller.ts +++ b/src/domain/letter/letter.controller.ts @@ -1,15 +1,14 @@ import { + Body, Controller, Get, - Param, Post, Query, + Request, UseInterceptors, } from '@nestjs/common'; -import { GetLetterDetailRequest } from './dto/request/get.detail'; import { HttpResponse } from '../dto/response'; import { ResponseValidationInterceptor } from 'src/interceptor/response.validation'; -import { GetLetterDetailResponse } from './dto/response/get.detail'; import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; import { GetLetterPageRequest } from './dto/request/get.page'; import { GetLetterPageResponse } from './dto/response/get.page'; @@ -17,6 +16,8 @@ import { LetterService } from './letter.service'; import { LetterCategory } from '@util/category'; import { PrepareResponse } from './dto/response/prepare'; import { PrepareRequest } from './dto/request/prepare'; +import { AddLetterRequest } from './dto/request/add.letter'; +import { AddLetterResponse } from './dto/response/add.letter'; @Controller('letter') export class LetterController { @@ -49,59 +50,79 @@ export class LetterController { }; } + @ApiOperation({ summary: '초대장 업로드 url 조회' }) + @ApiOkResponse({ + status: 200, + description: '성공', + type: PrepareResponse, + }) @Get('prepare-add') @UseInterceptors(new ResponseValidationInterceptor(PrepareResponse)) async prepareAddLetter( @Query() dto: PrepareRequest, + @Request() req ): Promise> { return { result: true, - data: await this.letterService.prepareAddLetter(dto), + data: await this.letterService.prepareAddLetter(dto,req.user), }; } -// @Get(':id') -// @ApiOperation({ summary: '초대장 상세 정보 조회' }) -// @ApiOkResponse({ -// status: 200, -// description: '성공', -// type: GetLetterDetailResponse, -// }) -// @UseInterceptors(new ResponseValidationInterceptor(GetLetterDetailResponse)) -// async getLetterDetail( -// @Param() dto: GetLetterDetailRequest, -// ): Promise> { -// return { -// result: true, -// data: { -// background: { -// path: 'https://s3.ap-northeast-1.wasabisys.com/bgr/mock01', -// width: 800, -// height: 1600, -// }, -// image: [ -// { -// path: 'https://s3.ap-northeast-1.wasabisys.com/cpn/mock01', -// width: 100, -// height: 80, -// x: 200, -// y: 800, -// }, -// ], -// text: [ -// { -// body: 'MOCK', -// size: 18, -// x: 200, -// y: 600, -// }, -// ], -// }, -// }; -// } - - - + @ApiOperation({ summary : '초대장 업로드'}) + @ApiOkResponse({ + status : 201, + description : '성공', + type : AddLetterResponse + }) @Post() - async addLetter() {} + @UseInterceptors(new ResponseValidationInterceptor(AddLetterResponse)) + async addLetter( + @Body() dto : AddLetterRequest, + @Request() req, + ) : Promise> { + return { + result : true, + data : await this.letterService.addLetter(dto,req.user) + } + } + + // @Get(':id') + // @ApiOperation({ summary: '초대장 상세 정보 조회' }) + // @ApiOkResponse({ + // status: 200, + // description: '성공', + // type: GetLetterDetailResponse, + // }) + // @UseInterceptors(new ResponseValidationInterceptor(GetLetterDetailResponse)) + // async getLetterDetail( + // @Param() dto: GetLetterDetailRequest, + // ): Promise> { + // return { + // result: true, + // data: { + // background: { + // path: 'https://s3.ap-northeast-1.wasabisys.com/bgr/mock01', + // width: 800, + // height: 1600, + // }, + // image: [ + // { + // path: 'https://s3.ap-northeast-1.wasabisys.com/cpn/mock01', + // width: 100, + // height: 80, + // x: 200, + // y: 800, + // }, + // ], + // text: [ + // { + // body: 'MOCK', + // size: 18, + // x: 200, + // y: 600, + // }, + // ], + // }, + // }; + // } } diff --git a/src/domain/letter/letter.service.ts b/src/domain/letter/letter.service.ts index b6ef795..dda3c61 100644 --- a/src/domain/letter/letter.service.ts +++ b/src/domain/letter/letter.service.ts @@ -1,5 +1,10 @@ import { StorageService } from '@storage/storage.service'; -import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { + BadRequestException, + ForbiddenException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { Enviroments } from '../dto/env'; import { v4 as uuidv4 } from 'uuid'; @@ -10,6 +15,8 @@ import { AddLetterResponse } from './dto/response/add.letter'; import { RedisClientService } from '@redis/redis.client.service'; import { User } from '@jwt/user'; import { randomString } from '@util/random'; +import { InsertLetterTrasnaction } from './transaction/insert.letter'; +import { booleanToYN } from '@util/yn'; @Injectable() export class LetterService { @@ -18,7 +25,11 @@ export class LetterService { private readonly componentBucket; private readonly letterBucket; private readonly urlExpires = 60; - constructor(private readonly storage: StorageService, private readonly redis : RedisClientService) { + constructor( + private readonly storage: StorageService, + private readonly redis: RedisClientService, + private readonly insertLetterTransaction: InsertLetterTrasnaction, + ) { const env = plainToInstance(Enviroments, process.env, { enableImplicitConversion: true, }); @@ -28,9 +39,10 @@ export class LetterService { this.letterBucket = env.LETTER_BUCKET; } - async prepareAddLetter({ - componentCount, - }: PrepareRequest, user:User): Promise { + async prepareAddLetter( + { componentCount }: PrepareRequest, + user: User, + ): Promise { const uuid = uuidv4(); const thumbnailUrl = await this.storage.generateUploadPresignedUrl({ bucket: this.thumbnailBucket, @@ -38,10 +50,10 @@ export class LetterService { expires: this.urlExpires, }); const letterUrl = await this.storage.generateUploadPresignedUrl({ - bucket : this.letterBucket, - key : uuid, - expires : this.urlExpires, - }) + bucket: this.letterBucket, + key: uuid, + expires: this.urlExpires, + }); const backgroundUrl = await this.storage.generateUploadPresignedUrl({ bucket: this.backGroundBucket, key: uuid, @@ -58,36 +70,108 @@ export class LetterService { ); } const sessionKey = randomString(5); - await this.redis.set(this.redis.generateKey(LetterService.name,`add-${user.id}`),{ - sessionKey : sessionKey, - objectKey : uuid, - },70) + await this.redis.set( + this.redis.generateKey(LetterService.name, `add-${user.id}`), + { + sessionKey: sessionKey, + objectKey: uuid, + componentCount, + }, + 70, + ); return { thumbnailUrl, letterUrl, backgroundUrl, componentUrls, expires: this.urlExpires, - sessionKey + sessionKey, }; } - async addLetter(dto : AddLetterRequest,user:User) : Promise { + async addLetter( + { category, title, body, commentYn, attendYn }: AddLetterRequest, + user: User, + ): Promise { //1. 세션키 획득 - const session = await this.redis.get<{sessionKey:string,objectKey:string}>(this.redis.generateKey(LetterService.name,`add-${user.id}`)) - if(!session) throw new BadRequestException('필수 요청이 누락되었습니다.') + const session = await this.redis.get<{ + sessionKey: string; + objectKey: string; + componentCount: number; + }>(this.redis.generateKey(LetterService.name, `add-${user.id}`)); + if (!session) throw new BadRequestException('필수 요청이 누락되었습니다.'); //2. 메타데이터 조회 - const { sessionKey,objectKey } = session; - const thubnailMeta = await this.storage.getObjectMetadata({bucket:this.thumbnailBucket,key:objectKey}) - const letterMeta = await this.storage.getObjectMetadata({bucket:this.letterBucket,key:objectKey}) - const backgroundMeta = await this.storage.getObjectMetadata({bucket:this.backGroundBucket,key:objectKey}) - if(!thubnailMeta || !letterMeta || !backgroundMeta) throw new ForbiddenException('파일을 찾을 수 없습니다.') - if(thubnailMeta['x-amz-session-key'] !== sessionKey ||letterMeta['x-amz-session-key'] !== sessionKey ||backgroundMeta['x-amz-session-key'] !== sessionKey ) { - //TODO: 업로드된 파일 제거 + const { sessionKey, objectKey, componentCount } = session; + const thubnailMeta = await this.storage.getObjectMetadata({ + bucket: this.thumbnailBucket, + key: objectKey, + }); + const letterMeta = await this.storage.getObjectMetadata({ + bucket: this.letterBucket, + key: objectKey, + }); + const backgroundMeta = await this.storage.getObjectMetadata({ + bucket: this.backGroundBucket, + key: objectKey, + }); + if (!thubnailMeta || !letterMeta || !backgroundMeta) + throw new ForbiddenException('파일을 찾을 수 없습니다.'); + if ( + thubnailMeta['x-amz-session-key'] !== sessionKey || + letterMeta['x-amz-session-key'] !== sessionKey || + backgroundMeta['x-amz-session-key'] !== sessionKey + ) { + await this.storage.deleteObject({ + bucket: this.thumbnailBucket, + key: objectKey, + }); + await this.storage.deleteObject({ + bucket: this.letterBucket, + key: objectKey, + }); + await this.storage.deleteObject({ + bucket: this.backGroundBucket, + key: objectKey, + }); + throw new UnauthorizedException('세션키가 일치하지 않습니다.'); + } + // 2-2. 아이템 메타데이터 조회 후 업로드된 아이템만 추출 + const componentPaths = []; + for(let i = 0; i ({attachmentPath : `${this.componentBucket}/${path}`})) + }), + }; } } diff --git a/src/domain/letter/transaction/insert.letter.ts b/src/domain/letter/transaction/insert.letter.ts new file mode 100644 index 0000000..b7a9ee0 --- /dev/null +++ b/src/domain/letter/transaction/insert.letter.ts @@ -0,0 +1,82 @@ +import { AttachmentRepository } from '@database/repository/attachment'; +import { LetterRepository } from '@database/repository/letter'; +import { Attachment } from '@database/repository/param/attachment'; +import { Letter } from '@database/repository/param/letter'; +import { BaseTransaction } from '@database/transaction.base'; +import { DataSource, EntityManager } from 'typeorm'; + +interface Input { + letter: Letter; + thumnailAttachment: Attachment; + backgroundAttachment: Attachment; + letterAttachment: Attachment; + componentAttachments: Array; +} + +export class InsertLetterTrasnaction extends BaseTransaction { + constructor( + private readonly ds: DataSource, + private readonly letterRepository: LetterRepository, + private readonly attachmentRepository: AttachmentRepository, + ) { + super(ds); + } + + protected async execute( + { + letter, + thumnailAttachment, + letterAttachment, + backgroundAttachment, + componentAttachments, + }: Input, + entityManager: EntityManager, + ): Promise { + const letterId = await this._insertLetter(letter, entityManager); + const attachmentIds = await this._insertAttachment( + [ + thumnailAttachment, + letterAttachment, + backgroundAttachment, + ...componentAttachments, + ], + entityManager, + ); + await this._insertLetterAttachment(letterId, attachmentIds, entityManager); + return letterId; + } + + private async _insertLetter( + letter, + entityManager: EntityManager, + ): Promise { + return (await this.letterRepository.insertLetter({ letter, entityManager })) + .identifiers[0].id; + } + private async _insertAttachment( + attachments, + entityManager: EntityManager, + ): Promise { + await this.attachmentRepository.bulkInsertAttachments({ + attachments, + entityManager, + }); + return await this.attachmentRepository.selectAttachmentIds({ + attachmentPaths: attachments.map((att) => att.attachmentPath), + entityManager, + }); + } + private async _insertLetterAttachment( + letterId: number, + attachmentIds: number[], + entityManager: EntityManager, + ) { + return await this.letterRepository.insertLetterAttachment({ + letterAttachments: attachmentIds.map((a) => ({ + letterId, + attachmentId: a, + })), + entityManager, + }); + } +} diff --git a/src/domain/user/user.module.ts b/src/domain/user/user.module.ts index adb23d2..21824c8 100644 --- a/src/domain/user/user.module.ts +++ b/src/domain/user/user.module.ts @@ -1,10 +1,10 @@ -import { Module } from "@nestjs/common"; -import { UserService } from "./user.service"; +import { Module } from '@nestjs/common'; +import { UserService } from './user.service'; @Module({ - imports : [], - controllers : [], - providers : [UserService], - exports : [UserService] + imports: [], + controllers: [], + providers: [UserService], + exports: [UserService], }) -export class UserModule {} \ No newline at end of file +export class UserModule {} diff --git a/src/domain/user/user.service.ts b/src/domain/user/user.service.ts index 75bc0cd..734fb73 100644 --- a/src/domain/user/user.service.ts +++ b/src/domain/user/user.service.ts @@ -1,31 +1,31 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; -import { randomString } from "@util/random"; +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { randomString } from '@util/random'; import { v4 as uuidv4 } from 'uuid'; @Injectable() export class UserService { - private readonly mock = [ - { - userId : 'ddab41a0-0fc7-4602-927b-40a681021ace', - nickName : 'tester', - phone : '01012341234', - pwd : '97385f8ee138c77ecbd815a3dda29bc40ecbfc16945d5bb9d5e65480aca3c9bc' - } - ] + private readonly mock = [ + { + userId: 'ddab41a0-0fc7-4602-927b-40a681021ace', + nickName: 'tester', + phone: '01012341234', + pwd: '97385f8ee138c77ecbd815a3dda29bc40ecbfc16945d5bb9d5e65480aca3c9bc', + }, + ]; - async getUser(phone:string) { - const user = this.mock.find((u) => u.phone === phone) - if(!user) throw new UnauthorizedException('존재하지 않는 회원입니다.') - return user; - } + async getUser(phone: string) { + const user = this.mock.find((u) => u.phone === phone); + if (!user) throw new UnauthorizedException('존재하지 않는 회원입니다.'); + return user; + } - async saveUser(phone:string,pwd:string) { - const newUser = { - userId : uuidv4(), - nickName : randomString(), - phone, - pwd - } - this.mock.push() - return newUser; - } -} \ No newline at end of file + async saveUser(phone: string, pwd: string) { + const newUser = { + userId: uuidv4(), + nickName: randomString(), + phone, + pwd, + }; + this.mock.push(); + return newUser; + } +} diff --git a/src/jwt/guard/user.guard.ts b/src/jwt/guard/user.guard.ts index ddf43de..705eb2f 100644 --- a/src/jwt/guard/user.guard.ts +++ b/src/jwt/guard/user.guard.ts @@ -1,7 +1,7 @@ import { - ExecutionContext, - Injectable, - UnauthorizedException, + ExecutionContext, + Injectable, + UnauthorizedException, } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; @@ -9,38 +9,38 @@ import { Request } from 'express'; @Injectable() export class UserGuard extends AuthGuard('jwt') { - constructor(private readonly jwtService: JwtService) { - super(); + constructor(private readonly jwtService: JwtService) { + super(); + } + + async canActivate(context: ExecutionContext): Promise { + const isActivated = (await super.canActivate(context)) as boolean; + if (!isActivated) { + throw new UnauthorizedException('Invalid token'); } - async canActivate(context: ExecutionContext): Promise { - const isActivated = (await super.canActivate(context)) as boolean; - if (!isActivated) { - throw new UnauthorizedException('Invalid token'); - } + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); - const request = context.switchToHttp().getRequest(); - const token = this.extractTokenFromHeader(request); + if (!token) { + throw new UnauthorizedException('토큰이 없습니다.'); + } - if (!token) { - throw new UnauthorizedException('토큰이 없습니다.'); - } + try { + const payload = await this.jwtService.verifyAsync(token); + request.user = payload; + } catch (error) { + throw new UnauthorizedException('토큰 검증 실패'); + } - try { - const payload = await this.jwtService.verifyAsync(token); - request.user = payload; - } catch (error) { - throw new UnauthorizedException('토큰 검증 실패'); - } + return true; + } - return true; - } + private extractTokenFromHeader(request: Request): string | null { + const authHeader = request.headers.authorization; + if (!authHeader) return null; - private extractTokenFromHeader(request: Request): string | null { - const authHeader = request.headers.authorization; - if (!authHeader) return null; - - const [type, token] = authHeader.split(' '); - return type === 'Bearer' ? token : null; - } + const [type, token] = authHeader.split(' '); + return type === 'Bearer' ? token : null; + } } diff --git a/src/jwt/strategy/user.strategy.ts b/src/jwt/strategy/user.strategy.ts index 71a5c3a..f205da3 100644 --- a/src/jwt/strategy/user.strategy.ts +++ b/src/jwt/strategy/user.strategy.ts @@ -1,4 +1,3 @@ - import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { UserService } from '../../user/user.service'; @@ -6,9 +5,12 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() export class UserStrategy extends PassportStrategy(Strategy) { - constructor(private userService: UserService, private secret : string) { + constructor( + private userService: UserService, + private secret: string, + ) { super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: secret, }); @@ -20,8 +22,8 @@ export class UserStrategy extends PassportStrategy(Strategy) { throw new UnauthorizedException(); } return { - id : user.userId, - nick_name : user.nickName, + id: user.userId, + nick_name: user.nickName, }; } } diff --git a/src/jwt/user.ts b/src/jwt/user.ts index fea08c0..4488ee6 100644 --- a/src/jwt/user.ts +++ b/src/jwt/user.ts @@ -1,3 +1,3 @@ export class User { - id : string; -} \ No newline at end of file + id: string; +} diff --git a/src/redis/redis.client.service.ts b/src/redis/redis.client.service.ts new file mode 100644 index 0000000..d153e2a --- /dev/null +++ b/src/redis/redis.client.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import Redis from 'ioredis'; + +@Injectable() +export class RedisClientService { + constructor( + private readonly redisClient: Redis, + private readonly project: string, + ) {} + + async set( + key: string, + value: string | number | Object, + expireInSeconds?: number, + ): Promise { + const serializedValue = + typeof value === 'string' ? value : JSON.stringify(value); + if (expireInSeconds) { + await this.redisClient.set(key, serializedValue, 'EX', expireInSeconds); + } else { + await this.redisClient.set(key, serializedValue); + } + } + + async get(key: string): Promise { + const result = await this.redisClient.get(key); + if (!result) return null; + + try { + return JSON.parse(result) as T; + } catch (e) { + return result as T; + } + } + + async delete(key: string): Promise { + return this.redisClient.del(key); + } + + async has(key: string): Promise { + const result = await this.redisClient.exists(key); + return result === 1; + } + + generateKey(service: string, key: string) { + return `${this.project}::${service}::${key}`; + } +} diff --git a/src/redis/redis.module.ts b/src/redis/redis.module.ts new file mode 100644 index 0000000..8bf0fcb --- /dev/null +++ b/src/redis/redis.module.ts @@ -0,0 +1,43 @@ +import { DynamicModule, Module, Provider } from '@nestjs/common'; +import Redis, { RedisOptions } from 'ioredis'; +import { RedisClientService } from './redis.client.service'; + +const REDIS_CLIENT = 'REDIS_CLIENT'; + +interface RedisModuleAsyncOptions { + project: string; + imports?: any[]; + inject?: any[]; + useFactory: (...args: any[]) => RedisOptions | Promise; + isGlobal?: boolean; +} + +@Module({}) +export class RedisClientModule { + static forRootAsync(options: RedisModuleAsyncOptions): DynamicModule { + const redisProvider: Provider = { + provide: REDIS_CLIENT, + useFactory: async (...args: any[]) => { + const redisOptions = await options.useFactory(...args); + return new Redis(redisOptions); // Redis 인스턴스 생성 + }, + inject: options.inject || [], // 의존성 주입 설정 + }; + + return { + module: RedisClientModule, + imports: options.imports || [], + providers: [ + redisProvider, // REDIS_CLIENT 토큰을 제공하는 프로바이더 + { + provide: RedisClientService, + useFactory: (redis: Redis) => + new RedisClientService(redis, options.project), // Redis 인스턴스를 주입받는 RedisClientService 생성 + inject: [REDIS_CLIENT], // REDIS_CLIENT로부터 Redis 인스턴스 주입 + }, + ], + exports: [RedisClientService], + global: options.isGlobal || false, + }; + } +} diff --git a/src/storage/storage.service.ts b/src/storage/storage.service.ts index 1f69837..a786d49 100644 --- a/src/storage/storage.service.ts +++ b/src/storage/storage.service.ts @@ -44,4 +44,43 @@ export class StorageService { return await this.s3.getSignedUrlPromise('getObject', params); } + + async getObjectMetadata(param: { + bucket: string; + key: string; + }): Promise { + const params = { + Bucket: param.bucket, + Key: param.key, + }; + + const metadata = await this.s3 + .headObject(params) + .promise() + .catch((err) => { + if (err.statusCode == 404) { + return undefined; + } + throw err; + }); + return metadata; + } + + async deleteObject(param: { bucket: string; key: string }): Promise { + const params = { + Bucket: param.bucket, + Key: param.key, + }; + + await this.s3 + .deleteObject(params) + .promise() + .catch((err) => { + if (err.statusCode === 404) { + console.log('Object not found, nothing to delete.'); + return; + } + throw err; + }); + } } diff --git a/src/util/random.ts b/src/util/random.ts index 947cc51..b150ada 100644 --- a/src/util/random.ts +++ b/src/util/random.ts @@ -1,8 +1,9 @@ -export const randomString = ( legnth : number = 10) : string => { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; -} \ No newline at end of file +export const randomString = (legnth: number = 10): string => { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; +}; diff --git a/src/util/yn.ts b/src/util/yn.ts index 280a2b4..cad6838 100644 --- a/src/util/yn.ts +++ b/src/util/yn.ts @@ -4,3 +4,7 @@ export const YN = { } as const; export type YN = (typeof YN)[keyof typeof YN]; + +export const booleanToYN(data:boolean) : YN { + return data === true ? YN.Y : YN.N +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b1b5bc1..b312c27 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,8 @@ "@storage/*": ["src/storage/*"], "@util/*": ["src/util/*"], "@database/*": ["src/database/*"], + "@redis/*":["src/redis/*"], + "@jwt/*":["src/jwt/*"], "@app/*": ["src/*"], } } diff --git a/yarn.lock b/yarn.lock index 90c10a8..3654817 100644 --- a/yarn.lock +++ b/yarn.lock @@ -585,6 +585,13 @@ __metadata: languageName: node linkType: hard +"@ioredis/commands@npm:^1.1.1": + version: 1.2.0 + resolution: "@ioredis/commands@npm:1.2.0" + checksum: 10c0/a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -1043,6 +1050,18 @@ __metadata: languageName: node linkType: hard +"@nestjs/jwt@npm:^10.2.0": + version: 10.2.0 + resolution: "@nestjs/jwt@npm:10.2.0" + dependencies: + "@types/jsonwebtoken": "npm:9.0.5" + jsonwebtoken: "npm:9.0.2" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + checksum: 10c0/81c5cbcb459122b175ad6b50dad83aab7d5dc3beb6122a56c7f985cc1c7838cd1c5eae9d630e95550b95a03e183502a183029e36ba51879c638bd0bad086c056 + languageName: node + linkType: hard + "@nestjs/mapped-types@npm:2.0.5": version: 2.0.5 resolution: "@nestjs/mapped-types@npm:2.0.5" @@ -1060,6 +1079,16 @@ __metadata: languageName: node linkType: hard +"@nestjs/passport@npm:^10.0.3": + version: 10.0.3 + resolution: "@nestjs/passport@npm:10.0.3" + peerDependencies: + "@nestjs/common": ^8.0.0 || ^9.0.0 || ^10.0.0 + passport: ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + checksum: 10c0/9e8a6103407852951625e75d0abd82a0f9786d4f27fc7036731ccbac39cbdb4e597a7313e53a266bb1fe1ec36c5193365abeb3264f5d285ba0aaeb23ee8e3f1b + languageName: node + linkType: hard + "@nestjs/platform-express@npm:^10.0.0": version: 10.4.5 resolution: "@nestjs/platform-express@npm:10.4.5" @@ -1469,6 +1498,24 @@ __metadata: languageName: node linkType: hard +"@types/jsonwebtoken@npm:*": + version: 9.0.7 + resolution: "@types/jsonwebtoken@npm:9.0.7" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/e1cd0e48fcae21b1d4378887a23453bd7212b480a131b11bcda2cdeb0687d03c9646ee5ba592e04cfaf76f7cc80f179950e627cdb3ebc90a5923bce49a35631a + languageName: node + linkType: hard + +"@types/jsonwebtoken@npm:9.0.5": + version: 9.0.5 + resolution: "@types/jsonwebtoken@npm:9.0.5" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/c582b8420586f3b9550f7e34992cb32be300bc953636f3b087ed9c180ce7ea5c2e4b35090be2d57f0d3168cc3ca1074932907caa2afe09f4e9c84cf5c0daefa8 + languageName: node + linkType: hard + "@types/lodash@npm:~4.14.195": version: 4.14.202 resolution: "@types/lodash@npm:4.14.202" @@ -1531,6 +1578,44 @@ __metadata: languageName: node linkType: hard +"@types/passport-jwt@npm:^4": + version: 4.0.1 + resolution: "@types/passport-jwt@npm:4.0.1" + dependencies: + "@types/jsonwebtoken": "npm:*" + "@types/passport-strategy": "npm:*" + checksum: 10c0/0ced0eaa7bb379d674821108d9bc6758223f1a5f2b9790ec78d3eaaccce6a58a424cf8ed22b53d813740ec53d929e21d92cf794ef0fb30c732866750763c0d7a + languageName: node + linkType: hard + +"@types/passport-strategy@npm:*": + version: 0.2.38 + resolution: "@types/passport-strategy@npm:0.2.38" + dependencies: + "@types/express": "npm:*" + "@types/passport": "npm:*" + checksum: 10c0/d7d2b1782a0845bd8914250aa9213a23c8d9c2225db46d854b77f2bf0129a789f46d4a5e9ad336eca277fc7e0a051c0a2942da5c864e7c6710763f102d9d4295 + languageName: node + linkType: hard + +"@types/passport@npm:*": + version: 1.0.16 + resolution: "@types/passport@npm:1.0.16" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/7120c1186c8c67e3818683b5b6a4439d102f67da93cc1c7d8f32484f7bf10e8438dd5de0bf571910b23d06caa43dd1ad501933b48618bfaf54e63219500993fe + languageName: node + linkType: hard + +"@types/passport@npm:^0": + version: 0.4.7 + resolution: "@types/passport@npm:0.4.7" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/58ca21800b7910385961b7a3dc9071fc9db6223242b96ff88d16c9d004ce7173524e7c17c63363595565af3f85ad932ee301efc8cc3e378bea172eebc9e07703 + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.16 resolution: "@types/qs@npm:6.9.16" @@ -2491,6 +2576,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -2822,6 +2914,13 @@ __metadata: languageName: node linkType: hard +"cluster-key-slot@npm:^1.1.0": + version: 1.1.2 + resolution: "cluster-key-slot@npm:1.1.2" + checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 + languageName: node + linkType: hard + "co@npm:^4.6.0": version: 4.6.0 resolution: "co@npm:4.6.0" @@ -3265,6 +3364,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -4524,6 +4632,8 @@ __metadata: "@nestjs/common": "npm:^10.0.0" "@nestjs/config": "npm:^3.3.0" "@nestjs/core": "npm:^10.0.0" + "@nestjs/jwt": "npm:^10.2.0" + "@nestjs/passport": "npm:^10.0.3" "@nestjs/platform-express": "npm:^10.0.0" "@nestjs/schematics": "npm:^10.0.0" "@nestjs/swagger": "npm:^7.4.2" @@ -4533,6 +4643,8 @@ __metadata: "@types/jest": "npm:^29.5.2" "@types/multer": "npm:^1.4.12" "@types/node": "npm:^20.3.1" + "@types/passport": "npm:^0" + "@types/passport-jwt": "npm:^4" "@types/supertest": "npm:^6.0.0" "@types/uuid": "npm:^10" "@typescript-eslint/eslint-plugin": "npm:^6.0.0" @@ -4544,8 +4656,11 @@ __metadata: eslint-config-prettier: "npm:^9.1.0" eslint-plugin-prettier: "npm:^5.2.1" eslint-plugin-unused-imports: "npm:^4.1.4" + ioredis: "npm:^5.4.1" jest: "npm:^29.5.0" mysql2: "npm:^3.11.3" + passport: "npm:^0.7.0" + passport-jwt: "npm:^4.0.1" prettier: "npm:^3.0.0" reflect-metadata: "npm:^0.2.0" rxjs: "npm:^7.8.1" @@ -4562,6 +4677,23 @@ __metadata: languageName: unknown linkType: soft +"ioredis@npm:^5.4.1": + version: 5.4.1 + resolution: "ioredis@npm:5.4.1" + dependencies: + "@ioredis/commands": "npm:^1.1.1" + cluster-key-slot: "npm:^1.1.0" + debug: "npm:^4.3.4" + denque: "npm:^2.1.0" + lodash.defaults: "npm:^4.2.0" + lodash.isarguments: "npm:^3.1.0" + redis-errors: "npm:^1.2.0" + redis-parser: "npm:^3.0.0" + standard-as-callback: "npm:^2.1.0" + checksum: 10c0/5d28b7c89a3cab5b76d75923d7d4ce79172b3a1ca9be690133f6e8e393a7a4b4ffd55513e618bbb5504fed80d9e1395c9d9531a7c5c5c84aa4c4e765cca75456 + languageName: node + linkType: hard + "iota-array@npm:^1.0.0": version: 1.0.0 resolution: "iota-array@npm:1.0.0" @@ -5433,6 +5565,45 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0": + version: 9.0.2 + resolution: "jsonwebtoken@npm:9.0.2" + dependencies: + jws: "npm:^3.2.2" + lodash.includes: "npm:^4.3.0" + lodash.isboolean: "npm:^3.0.3" + lodash.isinteger: "npm:^4.0.4" + lodash.isnumber: "npm:^3.0.3" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.once: "npm:^4.0.0" + ms: "npm:^2.1.1" + semver: "npm:^7.5.4" + checksum: 10c0/d287a29814895e866db2e5a0209ce730cbc158441a0e5a70d5e940eb0d28ab7498c6bf45029cc8b479639bca94056e9a7f254e2cdb92a2f5750c7f358657a131 + languageName: node + linkType: hard + +"jwa@npm:^1.4.1": + version: 1.4.1 + resolution: "jwa@npm:1.4.1" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/5c533540bf38702e73cf14765805a94027c66a0aa8b16bc3e89d8d905e61a4ce2791e87e21be97d1293a5ee9d4f3e5e47737e671768265ca4f25706db551d5e9 + languageName: node + linkType: hard + +"jws@npm:^3.2.2": + version: 3.2.2 + resolution: "jws@npm:3.2.2" + dependencies: + jwa: "npm:^1.4.1" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/e770704533d92df358adad7d1261fdecad4d7b66fa153ba80d047e03ca0f1f73007ce5ed3fbc04d2eba09ba6e7e6e645f351e08e5ab51614df1b0aa4f384dfff + languageName: node + linkType: hard + "keyv@npm:^4.5.3": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -5505,6 +5676,62 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 + languageName: node + linkType: hard + +"lodash.includes@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.includes@npm:4.3.0" + checksum: 10c0/7ca498b9b75bf602d04e48c0adb842dfc7d90f77bcb2a91a2b2be34a723ad24bc1c8b3683ec6b2552a90f216c723cdea530ddb11a3320e08fa38265703978f4b + languageName: node + linkType: hard + +"lodash.isarguments@npm:^3.1.0": + version: 3.1.0 + resolution: "lodash.isarguments@npm:3.1.0" + checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8 + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7 + languageName: node + linkType: hard + +"lodash.isinteger@npm:^4.0.4": + version: 4.0.4 + resolution: "lodash.isinteger@npm:4.0.4" + checksum: 10c0/4c3e023a2373bf65bf366d3b8605b97ec830bca702a926939bcaa53f8e02789b6a176e7f166b082f9365bfec4121bfeb52e86e9040cb8d450e64c858583f61b7 + languageName: node + linkType: hard + +"lodash.isnumber@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isnumber@npm:3.0.3" + checksum: 10c0/2d01530513a1ee4f72dd79528444db4e6360588adcb0e2ff663db2b3f642d4bb3d687051ae1115751ca9082db4fdef675160071226ca6bbf5f0c123dbf0aa12d + languageName: node + linkType: hard + +"lodash.isplainobject@npm:^4.0.6": + version: 4.0.6 + resolution: "lodash.isplainobject@npm:4.0.6" + checksum: 10c0/afd70b5c450d1e09f32a737bed06ff85b873ecd3d3d3400458725283e3f2e0bb6bf48e67dbe7a309eb371a822b16a26cca4a63c8c52db3fc7dc9d5f9dd324cbb + languageName: node + linkType: hard + +"lodash.isstring@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.isstring@npm:4.0.1" + checksum: 10c0/09eaf980a283f9eef58ef95b30ec7fee61df4d6bf4aba3b5f096869cc58f24c9da17900febc8ffd67819b4e29de29793190e88dc96983db92d84c95fa85d1c92 + languageName: node + linkType: hard + "lodash.memoize@npm:^4.1.2": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -5519,6 +5746,13 @@ __metadata: languageName: node linkType: hard +"lodash.once@npm:^4.0.0": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04 + languageName: node + linkType: hard + "lodash@npm:4.17.21, lodash@npm:^4.17.21, lodash@npm:~4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -5918,7 +6152,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.1.3": +"ms@npm:2.1.3, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 @@ -6352,6 +6586,34 @@ __metadata: languageName: node linkType: hard +"passport-jwt@npm:^4.0.1": + version: 4.0.1 + resolution: "passport-jwt@npm:4.0.1" + dependencies: + jsonwebtoken: "npm:^9.0.0" + passport-strategy: "npm:^1.0.0" + checksum: 10c0/d7e2b472d399f596a1db31310f8e63d10777ab7468b9a378c964156e5f0a772598b007417356ead578cfdaf60dc2bba39a55f0033ca865186fdb2a2b198e2e7e + languageName: node + linkType: hard + +"passport-strategy@npm:1.x.x, passport-strategy@npm:^1.0.0": + version: 1.0.0 + resolution: "passport-strategy@npm:1.0.0" + checksum: 10c0/cf4cd32e1bf2538a239651581292fbb91ccc83973cde47089f00d2014c24bed63d3e65af21da8ddef649a8896e089eb9c3ac9ca639f36c797654ae9ee4ed65e1 + languageName: node + linkType: hard + +"passport@npm:^0.7.0": + version: 0.7.0 + resolution: "passport@npm:0.7.0" + dependencies: + passport-strategy: "npm:1.x.x" + pause: "npm:0.0.1" + utils-merge: "npm:^1.0.1" + checksum: 10c0/08c940b86e4adbfe43e753f8097300a5a9d1ce9a3aa002d7b12d27770943a1a87202c54597c0f04dbfd4117d67de76303433577512fc19c7e364fec37b0d3fc5 + languageName: node + linkType: hard + "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -6411,6 +6673,13 @@ __metadata: languageName: node linkType: hard +"pause@npm:0.0.1": + version: 0.0.1 + resolution: "pause@npm:0.0.1" + checksum: 10c0/f362655dfa7f44b946302c5a033148852ed5d05f744bd848b1c7eae6a543f743e79c7751ee896ba519fd802affdf239a358bb2ea5ca1b1c1e4e916279f83ab75 + languageName: node + linkType: hard + "picocolors@npm:^1.0.0, picocolors@npm:^1.1.0": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -6709,6 +6978,22 @@ __metadata: languageName: node linkType: hard +"redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "redis-errors@npm:1.2.0" + checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7 + languageName: node + linkType: hard + +"redis-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "redis-parser@npm:3.0.0" + dependencies: + redis-errors: "npm:^1.0.0" + checksum: 10c0/ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f + languageName: node + linkType: hard + "reflect-metadata@npm:^0.2.0, reflect-metadata@npm:^0.2.1": version: 0.2.2 resolution: "reflect-metadata@npm:0.2.2" @@ -7218,6 +7503,13 @@ __metadata: languageName: node linkType: hard +"standard-as-callback@npm:^2.1.0": + version: 2.1.0 + resolution: "standard-as-callback@npm:2.1.0" + checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -8089,7 +8381,7 @@ __metadata: languageName: node linkType: hard -"utils-merge@npm:1.0.1": +"utils-merge@npm:1.0.1, utils-merge@npm:^1.0.1": version: 1.0.1 resolution: "utils-merge@npm:1.0.1" checksum: 10c0/02ba649de1b7ca8854bfe20a82f1dfbdda3fb57a22ab4a8972a63a34553cf7aa51bc9081cf7e001b035b88186d23689d69e71b510e610a09a4c66f68aa95b672