diff --git a/package-lock.json b/package-lock.json index b19dd50..9f2f15d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@nestjs/platform-express": "^8.0.0", "@nestjs/platform-socket.io": "^8.4.7", "@nestjs/swagger": "^5.0.9", + "@nestjs/throttler": "^3.0.0", "@nestjs/typeorm": "^8.1.4", "@nestjs/websockets": "^8.4.7", "@redis/client": "^1.2.0", @@ -42,6 +43,7 @@ "rxjs": "^7.2.0", "swagger-ui-express": "^4.4.0", "typeorm": "^0.3.7", + "uuid": "^8.3.2", "winston": "^3.8.1" }, "devDependencies": { @@ -54,6 +56,7 @@ "@types/jsonwebtoken": "^8.5.8", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "codecov": "^3.8.3", @@ -1830,6 +1833,19 @@ } } }, + "node_modules/@nestjs/throttler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-3.0.0.tgz", + "integrity": "sha512-E5aLstJ1a3yZE6AgcN+BgHLiRd8lonR5E4E4I3wzVHRGfgglHQS1sa2zEUuD/pdzLPlbI8pvVDJom8Z2D1oDug==", + "dependencies": { + "md5": "^2.2.1" + }, + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0", + "reflect-metadata": "^0.1.13" + } + }, "node_modules/@nestjs/typeorm": { "version": "8.1.4", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-8.1.4.tgz", @@ -2357,6 +2373,12 @@ "@types/superagent": "*" } }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", @@ -3464,6 +3486,14 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -4018,6 +4048,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", @@ -5859,6 +5897,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -7645,6 +7688,16 @@ "tmpl": "1.0.5" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12206,6 +12259,14 @@ "tslib": "2.4.0" } }, + "@nestjs/throttler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/throttler/-/throttler-3.0.0.tgz", + "integrity": "sha512-E5aLstJ1a3yZE6AgcN+BgHLiRd8lonR5E4E4I3wzVHRGfgglHQS1sa2zEUuD/pdzLPlbI8pvVDJom8Z2D1oDug==", + "requires": { + "md5": "^2.2.1" + } + }, "@nestjs/typeorm": { "version": "8.1.4", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-8.1.4.tgz", @@ -12678,6 +12739,12 @@ "@types/superagent": "*" } }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@types/yargs": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", @@ -13499,6 +13566,11 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -13930,6 +14002,11 @@ "which": "^2.0.1" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + }, "crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", @@ -15299,6 +15376,11 @@ "binary-extensions": "^2.0.0" } }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -16666,6 +16748,16 @@ "tmpl": "1.0.5" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/package.json b/package.json index bf07dbf..f1bad99 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nestjs/platform-express": "^8.0.0", "@nestjs/platform-socket.io": "^8.4.7", "@nestjs/swagger": "^5.0.9", + "@nestjs/throttler": "^3.0.0", "@nestjs/typeorm": "^8.1.4", "@nestjs/websockets": "^8.4.7", "@redis/client": "^1.2.0", @@ -57,6 +58,7 @@ "rxjs": "^7.2.0", "swagger-ui-express": "^4.4.0", "typeorm": "^0.3.7", + "uuid": "^8.3.2", "winston": "^3.8.1" }, "lint-staged": { @@ -74,6 +76,7 @@ "@types/jsonwebtoken": "^8.5.8", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "codecov": "^3.8.3", diff --git a/src/app.module.ts b/src/app.module.ts index 86e249a..af2a395 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { APP_FILTER } from '@nestjs/core'; import { DatabaseModule } from './database/database.module'; import { UsersModule } from './users/users.module'; import { SmsModule } from './sms/sms.module'; +import { ThrottlerModule } from '@nestjs/throttler'; @Module({ imports: [ @@ -66,7 +67,11 @@ import { SmsModule } from './sms/sms.module'; SocketModule, DatabaseModule.forRoot({ isTest: false }), UsersModule, - SmsModule + SmsModule, + ThrottlerModule.forRoot({ + ttl: process.env.NODE_ENV === 'prod' ? 300 : 60, + limit: 3 + }) ], providers: [ diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 390ffa5..bfb2147 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -21,12 +21,14 @@ import { ResponseRequestValidationDto } from './dtos/RequestValidation.response. import { RequestValidateNumberDto } from './dtos/ValidateNumber.request.dto'; import { ResponseValidateNumberDto } from './dtos/ValidateNumber.response.dto'; import { RegisterTokenGuard } from './guards/RegisterToken.guard'; +import { ThrottlerBehindProxyGuard } from './guards/TrottlerBehindProxy.guard'; @ApiTags('auth') @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} + @UseGuards(ThrottlerBehindProxyGuard) @ApiOperation({ summary: '휴대전화번호 인증번호를 요청한다.' }) @ApiBody({ type: RequestPhoneNumberDto }) @ApiResponse({ @@ -34,6 +36,10 @@ export class AuthController { description: '요청 성공시', type: ResponseRequestValidationDto }) + @ApiResponse({ + status: 429, + description: '과도한 요청을 보낼시에' + }) @Post('message/send') async requestPhoneValidationNumber( @Body() requestPhoneNumberDto: RequestPhoneNumberDto @@ -82,6 +88,7 @@ export class AuthController { ); } + @UseGuards(ThrottlerBehindProxyGuard) @ApiOperation({ summary: '슬랙 인증번호를 발송한다 (관리자 용 )' }) @ApiResponse({ status: 200, @@ -92,6 +99,10 @@ export class AuthController { status: 400, description: '슬랙에 들어와있는 유저가 아닐때 , 어드민 유저가 아닐 때' }) + @ApiResponse({ + status: 429, + description: '과도한 요청을 보낼시에' + }) @ApiBody({ type: RequestAdminSendValidationNumberDto }) @Post('/slack/send') async slackSendValidationNumber( diff --git a/src/auth/guards/TrottlerBehindProxy.guard.ts b/src/auth/guards/TrottlerBehindProxy.guard.ts new file mode 100644 index 0000000..51b2da0 --- /dev/null +++ b/src/auth/guards/TrottlerBehindProxy.guard.ts @@ -0,0 +1,29 @@ +// throttler-behind-proxy.guard.ts +// 고스락 백엔드 서버는 nginx 뒤에 프록시 형태로 연결되어있기 때문에 +// X-Forwarded-For 헤더값을 통해서 +// 요청한 사람의 원래 ip 주소를 가져와야합니다. +import { ThrottlerGuard } from '@nestjs/throttler'; +import { Injectable } from '@nestjs/common'; +import { Request } from 'express'; +import { v4 } from 'uuid'; + +@Injectable() +export class ThrottlerBehindProxyGuard extends ThrottlerGuard { + protected getTracker(req: Request): string { + if (process.env.NODE_ENV === 'prod') { + const clientProxyIps = req.headers['x-forwarded-for']; + if (!clientProxyIps) { + return v4(); + } + if (Array.isArray(clientProxyIps)) { + return clientProxyIps[0]; + } else { + return clientProxyIps; + } + } else { + return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs + } + } +} + +// app.controller.ts