From 7c37ad0031ced8d3453106cf1106ab36bd91906e Mon Sep 17 00:00:00 2001 From: overthestream Date: Fri, 26 Jul 2024 17:17:26 +0900 Subject: [PATCH 01/11] remove import sort --- biome.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/biome.json b/biome.json index 8e62735..058d0f4 100644 --- a/biome.json +++ b/biome.json @@ -1,8 +1,5 @@ { "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json", - "organizeImports": { - "enabled": true - }, "javascript": { "parser": { "unsafeParameterDecoratorsEnabled": true From 00b5ed1f440409b5321a0b3993da7aa051d80548 Mon Sep 17 00:00:00 2001 From: overthestream Date: Fri, 26 Jul 2024 17:17:40 +0900 Subject: [PATCH 02/11] add pino logger config --- package.json | 2 + src/app.module.ts | 47 ++++++++++++++++----- src/main.ts | 42 +++++++------------ yarn.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 156 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 245a7da..4f3d19a 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "passport-local": "^1.0.0", "pg": "^8.11.3", "pino": "^9.3.2", + "pino-http": "^10.2.0", + "pino-pretty": "^11.2.2", "pinpoint-node-agent": "^0.8.4-next.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", diff --git a/src/app.module.ts b/src/app.module.ts index 1777011..da5f65a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,20 +1,24 @@ +import { randomUUID } from 'node:crypto'; +import { MailerModule } from '@nestjs-modules/mailer'; import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { ScheduleModule } from '@nestjs/schedule'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LoggerModule } from 'nestjs-pino'; +import pino from 'pino'; +import { FileLogger } from 'typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { ConfigModule } from '@nestjs/config'; -import { MailerModule } from '@nestjs-modules/mailer'; -import { MailModule } from './domain/mail/mail.module'; +import { ChannelModule } from './channel/channel.module'; import { AuthModule } from './domain/auth/auth.module'; -import { FetchModule } from './fetch/fetch.module'; -import { ScheduleModule } from '@nestjs/schedule'; +import { CategoryModule } from './domain/category/category.module'; +import { MailModule } from './domain/mail/mail.module'; import { NoticeModule } from './domain/notice/notice.module'; -import { ChannelModule } from './channel/channel.module'; -import { UsersModule } from './domain/users/users.module'; -import { ScrapModule } from './domain/scrap/scrap.module'; import { NotificationModule } from './domain/notification/notification.module'; +import { ScrapModule } from './domain/scrap/scrap.module'; import { SubscribeModule } from './domain/subscribe/subscribe.module'; -import { CategoryModule } from './domain/category/category.module'; +import { UsersModule } from './domain/users/users.module'; +import { FetchModule } from './fetch/fetch.module'; @Module({ imports: [ @@ -28,6 +32,7 @@ import { CategoryModule } from './domain/category/category.module'; database: process.env.DB_DATABASE, autoLoadEntities: true, logging: true, + logger: new FileLogger('all', { logPath: './logs/orm.log' }), }), MailerModule.forRoot({ transport: { @@ -40,6 +45,28 @@ import { CategoryModule } from './domain/category/category.module'; }, }), ScheduleModule.forRoot(), + LoggerModule.forRoot({ + pinoHttp: [ + { + genReqId: (req, res) => { + const existingID = req.id ?? req.headers['x-request-id']; + if (existingID) return existingID; + const id = randomUUID(); + res.setHeader('x-request-id', id); + return id; + }, + transport: { target: 'pino-pretty' }, + customLogLevel: (req, res, err) => { + if (res.statusCode >= 500 || err) return 'error'; + if (res.statusCode >= 400) return 'warn'; + if (res.statusCode >= 300) return 'silent'; + + return 'info'; + }, + }, + pino(pino.destination('./logs/app.log')), + ], + }), MailModule, AuthModule, FetchModule, diff --git a/src/main.ts b/src/main.ts index 9ba3bf1..ffb4131 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,54 +1,44 @@ +import { ExceptionFilter, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { HttpExceptionFilter } from './filters/httpException.filter'; +import { AppModule } from './app.module'; import { ChannelService } from './channel/channel.service'; +import { HttpExceptionFilter } from './filters/httpException.filter'; import { InternalErrorFilter } from './filters/internalError.filter'; -import { - ExceptionFilter, - NotAcceptableException, - ValidationPipe, -} from '@nestjs/common'; import 'pinpoint-node-agent'; +import { Logger } from 'nestjs-pino'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bufferLogs: true }); const swaggerConfig = new DocumentBuilder() .setTitle('KUDOG API') .setDescription('API for KUDOG service') .setVersion('1.0') + .addBearerAuth() + .addSecurity('fake-auth', { + type: 'apiKey', + name: 'x-fake-auth', + in: 'header', + }) .build(); const docs = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('api', app, docs); - const channelService = app.get(ChannelService); - const filters: ExceptionFilter[] = []; + + const filters: ExceptionFilter[] = []; if (process.env.NODE_ENV === 'production') { + const channelService = app.get(ChannelService); await channelService.sendMessageToKudog('Server Deployed'); filters.push(new InternalErrorFilter(channelService)); } filters.push(new HttpExceptionFilter()); + app.useLogger(app.get(Logger)); app.useGlobalFilters(...filters); + app.useGlobalPipes(new ValidationPipe({ transform: true })); - app.useGlobalPipes( - new ValidationPipe({ - exceptionFactory: (errors) => { - const messages = errors - .map((error) => { - return `<${error.property}> ${Object.values(error.constraints).join( - ' ', - )}`; - }) - .join(' '); - return new NotAcceptableException( - `입력값이 유효하지 않습니다 - ${messages}`, - ); - }, - }), - ); await app.listen(3050); } bootstrap(); diff --git a/yarn.lock b/yarn.lock index ecb2e56..e8281d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3344,6 +3344,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:^2.0.7": + version: 2.0.20 + resolution: "colorette@npm:2.0.20" + checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 + languageName: node + linkType: hard + "combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -3633,6 +3640,13 @@ __metadata: languageName: node linkType: hard +"dateformat@npm:^4.6.3": + version: 4.6.3 + resolution: "dateformat@npm:4.6.3" + checksum: 10c0/e2023b905e8cfe2eb8444fb558562b524807a51cdfe712570f360f873271600b5c94aebffaf11efb285e2c072264a7cf243eadb68f3eba0f8cc85fb86cd25df6 + languageName: node + linkType: hard + "dayjs@npm:^1.11.9": version: 1.11.12 resolution: "dayjs@npm:1.11.12" @@ -4054,7 +4068,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.4.1": +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -4407,6 +4421,13 @@ __metadata: languageName: node linkType: hard +"fast-copy@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-copy@npm:3.0.2" + checksum: 10c0/02e8b9fd03c8c024d2987760ce126456a0e17470850b51e11a1c3254eed6832e4733ded2d93316c82bc0b36aeb991ad1ff48d1ba95effe7add7c3ab8d8eb554a + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -5160,6 +5181,13 @@ __metadata: languageName: node linkType: hard +"help-me@npm:^5.0.0": + version: 5.0.0 + resolution: "help-me@npm:5.0.0" + checksum: 10c0/054c0e2e9ae2231c85ab5e04f75109b9d068ffcc54e58fb22079822a5ace8ff3d02c66fd45379c902ad5ab825e5d2e1451fcc2f7eab1eb49e7d488133ba4cacb + languageName: node + linkType: hard + "hexoid@npm:^1.0.0": version: 1.0.0 resolution: "hexoid@npm:1.0.0" @@ -6299,6 +6327,13 @@ __metadata: languageName: node linkType: hard +"joycon@npm:^3.1.1": + version: 3.1.1 + resolution: "joycon@npm:3.1.1" + checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae + languageName: node + linkType: hard + "js-beautify@npm:^1.6.14": version: 1.15.1 resolution: "js-beautify@npm:1.15.1" @@ -6656,6 +6691,8 @@ __metadata: passport-local: "npm:^1.0.0" pg: "npm:^8.11.3" pino: "npm:^9.3.2" + pino-http: "npm:^10.2.0" + pino-pretty: "npm:^11.2.2" pinpoint-node-agent: "npm:^0.8.4-next.1" reflect-metadata: "npm:^0.1.13" rxjs: "npm:^7.8.1" @@ -8030,7 +8067,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.4.0": +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -8495,7 +8532,7 @@ __metadata: languageName: node linkType: hard -"pino-abstract-transport@npm:^1.2.0": +"pino-abstract-transport@npm:^1.0.0, pino-abstract-transport@npm:^1.2.0": version: 1.2.0 resolution: "pino-abstract-transport@npm:1.2.0" dependencies: @@ -8505,6 +8542,42 @@ __metadata: languageName: node linkType: hard +"pino-http@npm:^10.2.0": + version: 10.2.0 + resolution: "pino-http@npm:10.2.0" + dependencies: + get-caller-file: "npm:^2.0.5" + pino: "npm:^9.0.0" + pino-std-serializers: "npm:^7.0.0" + process-warning: "npm:^3.0.0" + checksum: 10c0/0b79cd3602531ee5043693e2a3ccf9d955bd93759e80c0b3a458b95b241f36ca8ebc72c8050b395e9d8fcb9581ebc18ecd6b7dc136526bebe924bc5c5079374d + languageName: node + linkType: hard + +"pino-pretty@npm:^11.2.2": + version: 11.2.2 + resolution: "pino-pretty@npm:11.2.2" + dependencies: + colorette: "npm:^2.0.7" + dateformat: "npm:^4.6.3" + fast-copy: "npm:^3.0.2" + fast-safe-stringify: "npm:^2.1.1" + help-me: "npm:^5.0.0" + joycon: "npm:^3.1.1" + minimist: "npm:^1.2.6" + on-exit-leak-free: "npm:^2.1.0" + pino-abstract-transport: "npm:^1.0.0" + pump: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + secure-json-parse: "npm:^2.4.0" + sonic-boom: "npm:^4.0.1" + strip-json-comments: "npm:^3.1.1" + bin: + pino-pretty: bin.js + checksum: 10c0/3ce1769907886a5584f6c8123d9bc987712ad10a375797733a0fe95a238df587dac8e2b709bab291c4e30d41b0cf65808c708c96f8eb98b2778b6df60afa7e66 + languageName: node + linkType: hard + "pino-std-serializers@npm:^7.0.0": version: 7.0.0 resolution: "pino-std-serializers@npm:7.0.0" @@ -8512,7 +8585,7 @@ __metadata: languageName: node linkType: hard -"pino@npm:^9.3.2": +"pino@npm:^9.0.0, pino@npm:^9.3.2": version: 9.3.2 resolution: "pino@npm:9.3.2" dependencies: @@ -8648,6 +8721,13 @@ __metadata: languageName: node linkType: hard +"process-warning@npm:^3.0.0": + version: 3.0.0 + resolution: "process-warning@npm:3.0.0" + checksum: 10c0/60f3c8ddee586f0706c1e6cb5aa9c86df05774b9330d792d7c8851cf0031afd759d665404d07037e0b4901b55c44a423f07bdc465c63de07d8d23196bb403622 + languageName: node + linkType: hard + "process-warning@npm:^4.0.0": version: 4.0.0 resolution: "process-warning@npm:4.0.0" @@ -8878,6 +8958,16 @@ __metadata: languageName: node linkType: hard +"pump@npm:^3.0.0": + version: 3.0.0 + resolution: "pump@npm:3.0.0" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/bbdeda4f747cdf47db97428f3a135728669e56a0ae5f354a9ac5b74556556f5446a46f720a8f14ca2ece5be9b4d5d23c346db02b555f46739934cc6c093a5478 + languageName: node + linkType: hard + "punycode.js@npm:2.3.1": version: 2.3.1 resolution: "punycode.js@npm:2.3.1" @@ -9362,6 +9452,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.4.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10c0/f57eb6a44a38a3eeaf3548228585d769d788f59007454214fab9ed7f01fbf2e0f1929111da6db28cf0bcc1a2e89db5219a59e83eeaec3a54e413a0197ce879e4 + languageName: node + linkType: hard + "selderee@npm:^0.11.0": version: 0.11.0 resolution: "selderee@npm:0.11.0" From e56caf978c8dbb97cb790b7ff7877824d303fd49 Mon Sep 17 00:00:00 2001 From: overthestream Date: Fri, 26 Jul 2024 20:39:02 +0900 Subject: [PATCH 03/11] logger configs --- src/app.module.ts | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index da5f65a..6349685 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,7 +5,6 @@ import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; import { LoggerModule } from 'nestjs-pino'; -import pino from 'pino'; import { FileLogger } from 'typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -46,26 +45,31 @@ import { FetchModule } from './fetch/fetch.module'; }), ScheduleModule.forRoot(), LoggerModule.forRoot({ - pinoHttp: [ - { - genReqId: (req, res) => { - const existingID = req.id ?? req.headers['x-request-id']; - if (existingID) return existingID; - const id = randomUUID(); - res.setHeader('x-request-id', id); - return id; - }, - transport: { target: 'pino-pretty' }, - customLogLevel: (req, res, err) => { - if (res.statusCode >= 500 || err) return 'error'; - if (res.statusCode >= 400) return 'warn'; - if (res.statusCode >= 300) return 'silent'; + pinoHttp: { + genReqId: (req, res) => { + const existingID = req.id ?? req.headers['x-request-id']; + if (existingID) return existingID; + const id = randomUUID(); + res.setHeader('x-request-id', id); + return id; + }, + transport: { + targets: [ + { + target: 'pino/file', + options: { destination: './logs/app.log', mkdir: true }, + }, + ], + }, + customLogLevel: (req, res, err) => { + if (res.statusCode >= 500 || err) return 'error'; + if (res.statusCode >= 400) return 'warn'; + if (res.statusCode >= 300) return 'silent'; - return 'info'; - }, + return 'info'; }, - pino(pino.destination('./logs/app.log')), - ], + redact: ['req.body.password', 'req.headers.authorization'], + }, }), MailModule, AuthModule, From 5227b4d9671d5b7cbb019db6b7854b30e82b8a1d Mon Sep 17 00:00:00 2001 From: overthestream Date: Sat, 27 Jul 2024 02:55:18 +0900 Subject: [PATCH 04/11] refactor structure + custom exceptions --- .github/workflows/cicd.yml | 2 + package.json | 10 +- src/app.controller.ts | 2 +- src/app.module.ts | 2 +- src/asset/error.json | 120 +++++++++ .../apiKudogExceptionRespone.decorator.ts | 38 +++ src/common/decorators/docs/auth.decorator.ts | 159 ++++++++++++ .../decorators/docs/category.decorator.ts | 70 ++++++ .../decorators/docs/common.decorator.ts | 15 +- src/common/decorators/docs/index.ts | 8 + src/common/decorators/docs/mail.decorator.ts | 46 ++++ .../decorators/docs/notice.decorator.ts | 136 +++++++++++ .../decorators/docs/notification.decorator.ts | 99 ++++++++ src/common/decorators/docs/scrap.decorator.ts | 119 +++++++++ .../decorators/docs/subscribe.decorator.ts | 152 ++++++++++++ src/common/decorators/docs/user.decorator.ts | 43 ++++ src/{ => common}/decorators/index.ts | 1 + .../decorators/injectAccessUser.decorator.ts | 2 +- .../decorators/injectLocalUser.decorator.ts | 0 .../decorators/injectRefreshUser.decorator.ts | 2 +- src/common/decorators/namedController.ts | 6 + .../decorators/usePagination.decorator.ts | 6 +- src/{interfaces => common/dtos}/pageQuery.ts | 0 .../dtos}/pageResponse.ts | 0 src/{interfaces => common/types}/auth.ts | 0 src/common/types/method.ts | 4 + src/{ => common}/utils/date.ts | 0 src/common/utils/exception.ts | 29 +++ src/decorators/docs/auth.decorator.ts | 217 ----------------- src/decorators/docs/category.decorator.ts | 80 ------ src/decorators/docs/index.ts | 1 - src/decorators/docs/mail.decorator.ts | 75 ------ src/decorators/docs/notice.decorator.ts | 176 -------------- src/decorators/docs/notification.decorator.ts | 130 ---------- src/decorators/docs/scrap.decorator.ts | 180 -------------- src/decorators/docs/subscribe.decorator.ts | 230 ------------------ src/decorators/docs/user.decorator.ts | 58 ----- src/domain/auth/auth.controller.ts | 62 +++-- src/domain/auth/auth.module.ts | 10 +- src/domain/auth/auth.service.ts | 14 +- .../auth}/entities/changePwd.entity.ts | 2 +- .../entities/emailAuthentication.entity.ts | 0 .../auth}/entities/refreshToken.entity.ts | 0 .../auth/passport/accessToken.strategy.ts | 12 +- src/domain/auth/passport/local.strategy.ts | 8 +- .../auth/passport/refreshToken.strategy.ts | 12 +- src/domain/category/category.controller.ts | 27 +- src/{ => domain}/channel/channel.module.ts | 0 src/{ => domain}/channel/channel.service.ts | 36 ++- .../channel/dtos/notification.dto.ts | 0 src/domain/mail/mail.controller.ts | 20 +- src/domain/mail/mail.service.ts | 2 +- .../notice/dtos/NoticeInfoResponse.dto.ts | 7 - src/domain/notice/notice.controller.ts | 58 ++--- src/domain/notice/notice.module.ts | 8 +- src/domain/notice/notice.service.ts | 14 +- .../notification/notification.controller.ts | 57 ++--- .../notification/notification.module.ts | 10 +- .../notification/notification.service.ts | 16 +- src/domain/scrap/scrap.controller.ts | 54 ++-- src/domain/scrap/scrap.service.ts | 6 +- src/domain/subscribe/subscribe.controller.ts | 68 +++--- src/domain/subscribe/subscribe.service.ts | 16 +- src/domain/users/users.controller.ts | 27 +- src/entities/category.entity.ts | 8 +- src/entities/index.ts | 6 +- src/enums/categoryMap.enum.ts | 12 - src/enums/index.ts | 1 - src/{interfaces => fetch}/dto.ts | 0 src/fetch/fetch.module.ts | 8 +- src/fetch/fetch.service.ts | 4 +- src/fetch/fetch.ts | 6 +- src/{interfaces => fetch}/urls.ts | 0 src/filters/httpException.filter.ts | 23 +- src/filters/internalError.filter.ts | 37 +-- src/interfaces/docsException.ts | 18 -- src/main.ts | 6 +- 77 files changed, 1329 insertions(+), 1564 deletions(-) create mode 100644 src/asset/error.json create mode 100644 src/common/decorators/apiKudogExceptionRespone.decorator.ts create mode 100644 src/common/decorators/docs/auth.decorator.ts create mode 100644 src/common/decorators/docs/category.decorator.ts rename src/{ => common}/decorators/docs/common.decorator.ts (63%) create mode 100644 src/common/decorators/docs/index.ts create mode 100644 src/common/decorators/docs/mail.decorator.ts create mode 100644 src/common/decorators/docs/notice.decorator.ts create mode 100644 src/common/decorators/docs/notification.decorator.ts create mode 100644 src/common/decorators/docs/scrap.decorator.ts create mode 100644 src/common/decorators/docs/subscribe.decorator.ts create mode 100644 src/common/decorators/docs/user.decorator.ts rename src/{ => common}/decorators/index.ts (83%) rename src/{ => common}/decorators/injectAccessUser.decorator.ts (84%) rename src/{ => common}/decorators/injectLocalUser.decorator.ts (100%) rename src/{ => common}/decorators/injectRefreshUser.decorator.ts (82%) create mode 100644 src/common/decorators/namedController.ts rename src/{ => common}/decorators/usePagination.decorator.ts (79%) rename src/{interfaces => common/dtos}/pageQuery.ts (100%) rename src/{interfaces => common/dtos}/pageResponse.ts (100%) rename src/{interfaces => common/types}/auth.ts (100%) create mode 100644 src/common/types/method.ts rename src/{ => common}/utils/date.ts (100%) create mode 100644 src/common/utils/exception.ts delete mode 100644 src/decorators/docs/auth.decorator.ts delete mode 100644 src/decorators/docs/category.decorator.ts delete mode 100644 src/decorators/docs/index.ts delete mode 100644 src/decorators/docs/mail.decorator.ts delete mode 100644 src/decorators/docs/notice.decorator.ts delete mode 100644 src/decorators/docs/notification.decorator.ts delete mode 100644 src/decorators/docs/scrap.decorator.ts delete mode 100644 src/decorators/docs/subscribe.decorator.ts delete mode 100644 src/decorators/docs/user.decorator.ts rename src/{ => domain/auth}/entities/changePwd.entity.ts (89%) rename src/{ => domain/auth}/entities/emailAuthentication.entity.ts (100%) rename src/{ => domain/auth}/entities/refreshToken.entity.ts (100%) rename src/{ => domain}/channel/channel.module.ts (100%) rename src/{ => domain}/channel/channel.service.ts (70%) rename src/{ => domain}/channel/dtos/notification.dto.ts (100%) delete mode 100644 src/enums/categoryMap.enum.ts delete mode 100644 src/enums/index.ts rename src/{interfaces => fetch}/dto.ts (100%) rename src/{interfaces => fetch}/urls.ts (100%) delete mode 100644 src/interfaces/docsException.ts diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 1ab0df7..9cc0ea6 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -67,6 +67,8 @@ jobs: echo "FCM_CLIENT_CERT_URL=${{ secrets.FCM_CLIENT_CERT_URL }}" >> .env echo "FCM_UNIVERSE_DOMAIN=${{ secrets.FCM_UNIVERSE_DOMAIN }}" >> .env echo "PY_TOKEN=${{ secrets.PY_TOKEN }}" >> .env + echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env + echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env cat .env - name: Run Docker run: | diff --git a/package.json b/package.json index 4f3d19a..131e1b6 100644 --- a/package.json +++ b/package.json @@ -70,11 +70,7 @@ "typescript": "^5.1.3" }, "jest": { - "moduleFileExtensions": [ - "js", - "json", - "ts" - ], + "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "moduleNameMapper": { "@/(.*)$": "/src/$1" @@ -83,9 +79,7 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverageFrom": [ - "**/*.(t|j)s" - ], + "collectCoverageFrom": ["**/*.(t|j)s"], "coverageDirectory": "../coverage", "testEnvironment": "node" }, diff --git a/src/app.controller.ts b/src/app.controller.ts index 5d8a91a..e56c003 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; import { ApiTags } from '@nestjs/swagger'; +import { AppService } from './app.service'; @Controller() @ApiTags('health check') diff --git a/src/app.module.ts b/src/app.module.ts index 6349685..69cd691 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,9 +8,9 @@ import { LoggerModule } from 'nestjs-pino'; import { FileLogger } from 'typeorm'; import { AppController } from './app.controller'; import { AppService } from './app.service'; -import { ChannelModule } from './channel/channel.module'; import { AuthModule } from './domain/auth/auth.module'; import { CategoryModule } from './domain/category/category.module'; +import { ChannelModule } from './domain/channel/channel.module'; import { MailModule } from './domain/mail/mail.module'; import { NoticeModule } from './domain/notice/notice.module'; import { NotificationModule } from './domain/notification/notification.module'; diff --git a/src/asset/error.json b/src/asset/error.json new file mode 100644 index 0000000..9e7e296 --- /dev/null +++ b/src/asset/error.json @@ -0,0 +1,120 @@ +{ + "EMAIL_VALIDATION_EXPIRED": { + "errorCode": 4000, + "statusCode": 400, + "name": "EMAIL_VALIDATION_EXPIRED", + "message": "이메일 인증이 만료되었습니다. 다시 인증해주세요." + }, + "EMAIL_NOT_VALIDATED": { + "errorCode": 4001, + "statusCode": 400, + "name": "EMAIL_NOT_VALIDATED", + "message": "인증되지 않은 이메일입니다." + }, + "EMAIL_ALREADY_USED": { + "errorCode": 4002, + "statusCode": 400, + "name": "EMAIL_ALREADY_USED", + "message": "이미 사용중인 이메일입니다." + }, + "EMAIL_NOT_IN_KOREA_DOMAIN": { + "errorCode": 4003, + "statusCode": 400, + "name": "EMAIL_NOT_IN_KOREA_DOMAIN", + "message": "korea.ac.kr 이메일이 아닙니다." + }, + "PASSWORD_INVALID_FORMAT": { + "errorCode": 4004, + "statusCode": 400, + "name": "PASSWORD_INVALID_FORMAT", + "message": "비밀번호는 6~16자의 영문 소문자와 숫자로만 입력해주세요." + }, + "CODE_NOT_CORRECT": { + "errorCode": 4005, + "statusCode": 400, + "name": "CODE_NOT_CORRECT", + "message": "인증 코드가 일치하지 않습니다." + }, + "CODE_EXPIRED": { + "errorCode": 4006, + "statusCode": 400, + "name": "CODE_EXPIRED", + "message": "인증 코드가 만료되었습니다. 다시 인증을 시도해주세요." + }, + "CODE_NOT_VALIDATED": { + "errorCode": 4007, + "statusCode": 400, + "name": "CODE_NOT_VALIDATED", + "message": "인증되지 않은 코드입니다." + }, + "CODE_VALIDATION_EXPIRED": { + "errorCode": 4008, + "statusCode": 400, + "name": "CODE_VALIDATION_EXPIRED", + "message": "인증 코드가 만료되었습니다. 다시 인증을 시도해주세요." + }, + + + "LOGIN_FAILED": { + "errorCode": 4010, + "statusCode": 401, + "name": "LOGIN_FAILED", + "message": "이메일 또는 비밀번호가 일치하지 않습니다." + }, + "TODO_REFRESH": { + "errorCode": 4011, + "statusCode": 401, + "name": "TODO_REFRESH", + "message": "할." + }, + "INVALID_ACCESS_TOKEN": { + "errorCode": 4012, + "statusCode": 401, + "name": "INVALID_ACCESS_TOKEN", + "message": "유효하지 않은 토큰입니다." + }, + "ACCESS_TOKEN_EXPIRED":{ + "errorCode": 4013, + "statusCode": 401, + "name": "TOKEN_EXPIRED", + "message": "ACCESS TOKEN 만료. 리프레시를 시도해주세요" + }, + "USER_NOT_FOUND": { + "errorCode": 4040, + "statusCode": 404, + "name": "USER_NOT_FOUND", + "message": "사용자를 찾을 수 없습니다." + }, + + "INVALID_PAGE_QUERY": { + "errorCode": 4060, + "statusCode": 406, + "name": "INVALID_PAGE_QUERY", + "message": "page 또는 pageSize 값이 잘못되었습니다. 자연수값이어야 합니다." + }, + + "TODO_INVALID": { + "errorCode": 4061, + "statusCode": 406, + "name": "TODO_INVALID", + "message": "할." + }, + "TOO_MANY_REQUESTS": { + "errorCode": 4290, + "statusCode": 429, + "name": "TOO_MANY_REQUESTS", + "message": "잠시 후 다시 시도해주세요." + }, + "INTERNAL_SERVER_ERROR": { + "errorCode": 5000, + "statusCode": 500, + "name": "INTERNAL_SERVER_ERROR", + "message": "Internal Server Error" + }, + "EMAIL_SEND_FAILED": { + "errorCode": 5100, + "statusCode": 510, + "name": "EMAIL_SEND_FAILED", + "message": "이메일 전송에 실패했습니다. 잠시 후에 다시 시도해주세요." + } +} diff --git a/src/common/decorators/apiKudogExceptionRespone.decorator.ts b/src/common/decorators/apiKudogExceptionRespone.decorator.ts new file mode 100644 index 0000000..ba31ade --- /dev/null +++ b/src/common/decorators/apiKudogExceptionRespone.decorator.ts @@ -0,0 +1,38 @@ +import { + EXCEPTIONS, + type ExceptionNames, + HttpException, +} from '@/common/utils/exception'; +import { applyDecorators } from '@nestjs/common'; +import { + ApiExtraModels, + ApiResponse, + type ApiResponseOptions, +} from '@nestjs/swagger'; + +export function ApiKudogExceptionResponse(names: ExceptionNames[]) { + const examples = names.reduce( + (acc: { [key: number]: ApiResponseOptions | undefined }, curr) => { + const exception = EXCEPTIONS[curr]; + const statusCode = exception.statusCode; + const responseOptions: ApiResponseOptions = acc[statusCode] ?? { + status: statusCode, + content: { + 'application/json': { + examples: {}, + }, + }, + }; + responseOptions.content['application/json'].examples[curr] = { + value: exception, + }; + acc[statusCode] = responseOptions; + return acc; + }, + {}, + ); + const decorators = Object.keys(examples).map((key) => + ApiResponse(examples[key]), + ); + return applyDecorators(ApiExtraModels(HttpException), ...decorators); +} diff --git a/src/common/decorators/docs/auth.decorator.ts b/src/common/decorators/docs/auth.decorator.ts new file mode 100644 index 0000000..d8a6df8 --- /dev/null +++ b/src/common/decorators/docs/auth.decorator.ts @@ -0,0 +1,159 @@ +import type { MethodNames } from '@/common/types/method'; +import type { AuthController } from '@/domain/auth/auth.controller'; +import { + ApiBody, + ApiCreatedResponse, + ApiHeader, + ApiOkResponse, + ApiOperation, +} from '@nestjs/swagger'; +import { LoginRequestDto } from 'src/domain/auth/dtos/loginRequestDto'; +import { TokenResponseDto } from 'src/domain/auth/dtos/tokenResponse.dto'; +import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; + +type AuthEndpoints = MethodNames; + +const AuthDocsMap: Record = { + login: [ + ApiOperation({ + description: + 'portal email, password를 통해 로그인, access, refresh JWT 발급', + summary: '로그인', + }), + ApiBody({ + type: LoginRequestDto, + description: 'portal email을 이용하여 로그인합니다.', + }), + ApiCreatedResponse({ + description: '로그인 성공', + type: TokenResponseDto, + }), + ApiKudogExceptionResponse(['LOGIN_FAILED']), + ], + signup: [ + ApiOperation({ + description: '회원가입', + summary: '회원가입', + }), + ApiCreatedResponse({ + description: '회원가입 성공 ', + type: TokenResponseDto, + }), + ApiKudogExceptionResponse([ + 'EMAIL_VALIDATION_EXPIRED', + 'EMAIL_NOT_VALIDATED', + 'EMAIL_ALREADY_USED', + 'EMAIL_NOT_IN_KOREA_DOMAIN', + 'PASSWORD_INVALID_FORMAT', + //TODO: ERROR instantize + 'TODO_INVALID', + ]), + ], + refresh: [ + ApiHeader({ + description: + 'Authorization header에 Bearer token 형태로 refresh token을 넣어주세요.', + name: 'authorization', + required: true, + }), + ApiOperation({ + description: + 'refresh token을 통해 access token 재발급, refresh token도 회전됩니다.', + summary: '토큰 재발급', + }), + ApiCreatedResponse({ + description: '토큰 재발급 성공', + type: TokenResponseDto, + }), + //TODO:TODO: + ApiKudogExceptionResponse(['TODO_REFRESH']), + ], + logout: [ + ApiOperation({ + summary: '로그아웃', + description: + 'refresh token을 삭제합니다. storage에서 두 토큰을 삭제해주세요. authorization header에 Bearer ${accessToken} 을 담아주세요.', + }), + ApiHeader({ + description: + 'Authorization header에 Bearer token 형태로 refresh token을 넣어주세요.', + name: 'authorization', + required: true, + }), + ApiKudogExceptionResponse(['INVALID_ACCESS_TOKEN', 'USER_NOT_FOUND']), + ApiOkResponse({ + description: 'logout 성공', + }), + ], + deleteUser: [ + ApiOperation({ + summary: '회원 탈퇴', + description: + '회원 탈퇴합니다. authorization header에 Bearer ${accessToken} 을 담아주세요.', + }), + ApiKudogExceptionResponse(['INVALID_ACCESS_TOKEN']), + ApiOkResponse({ + description: '회원 탈퇴 성공', + }), + ], + changePwdRequest: [ + ApiOperation({ + summary: '비밀번호 변경 이메일 인증 요청', + description: '비밀번호를 변경하기 위하여 이메일 인증을 요청합니다.', + }), + ApiCreatedResponse({ + description: '이메일 전송 성공. 3분 안에 인증 코드를 입력해주세요.', + }), + ApiKudogExceptionResponse([ + 'USER_NOT_FOUND', + 'TOO_MANY_REQUESTS', + 'EMAIL_NOT_IN_KOREA_DOMAIN', + 'TODO_INVALID', + 'EMAIL_SEND_FAILED', + ]), + ], + verifyChangePwdCode: [ + ApiOperation({ + summary: '비밀번호 변경 인증 코드 확인', + description: + '이메일로 전송된 인증 코드를 확인합니다. 제한시간은 3분이며, 인증 이후 10분 안에 비밀번호를 변경할 수 있습니다.', + }), + ApiCreatedResponse({ + description: '인증 성공, 비밀번호를 10분간 변경할 수 있습니다.', + }), + ApiKudogExceptionResponse([ + 'CODE_EXPIRED', + 'CODE_NOT_CORRECT', + 'TODO_INVALID', + ]), + ], + changePassword: [ + ApiOperation({ + summary: '비밀번호 변경', + description: + '비밀번호를 변경합니다. 인증 코드 확인 이후 10분 안에 비밀번호를 변경해주세요.', + }), + ApiOkResponse({ description: '비밀번호 변경 성공' }), + ApiKudogExceptionResponse([ + 'USER_NOT_FOUND', + 'CODE_NOT_VALIDATED', + 'CODE_EXPIRED', + 'TODO_INVALID', + ]), + ], +}; + +export function AuthDocs(target) { + for (const key in AuthDocsMap) { + const methodDecorators = AuthDocsMap[key as keyof typeof AuthDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/common/decorators/docs/category.decorator.ts b/src/common/decorators/docs/category.decorator.ts new file mode 100644 index 0000000..faa407b --- /dev/null +++ b/src/common/decorators/docs/category.decorator.ts @@ -0,0 +1,70 @@ +import type { MethodNames } from '@/common/types/method'; +import type { CategoryController } from '@/domain/category/category.controller'; +import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; +import { ProviderListResponseDto } from 'src/domain/category/dtos/ProviderListResponse.dto'; +import { CategoryListResponseDto } from 'src/domain/category/dtos/categoryListResponse.dto'; +import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; + +type CategoryEndpoints = MethodNames; + +const CategoryDocsMap: Record = { + getProviders: [ + ApiOperation({ + summary: '학부 리스트 조회', + description: + 'DB의 학부 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiOkResponse({ + description: '학부 리스트', + type: [ProviderListResponseDto], + }), + ApiKudogExceptionResponse(['ACCESS_TOKEN_EXPIRED']), + ], + getCategories: [ + ApiOperation({ + summary: '학부 소속 카테고리 리스트 조회', + description: + 'DB의 학부 소속 카테고리 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiParam({ + name: 'id', + description: '학부 id', + type: Number, + required: true, + example: 1, + }), + ApiOkResponse({ + description: '스크랩학부 소속 카테고리들', + type: [CategoryListResponseDto], + }), + ApiKudogExceptionResponse(['ACCESS_TOKEN_EXPIRED', 'TODO_INVALID']), + ], + getBookmarkedProviders: [ + ApiOperation({ + summary: '즐겨찾는 학부 목록 조회', + description: + '유저의 즐겨찾는 학과 목록 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiOkResponse({ + description: '사용자의 즐겨찾는 학과 목록', + type: [ProviderListResponseDto], + }), + ApiKudogExceptionResponse(['ACCESS_TOKEN_EXPIRED']), + ], +}; + +export function CategoryDocs(target) { + for (const key in CategoryDocsMap) { + const methodDecorators = + CategoryDocsMap[key as keyof typeof CategoryDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/decorators/docs/common.decorator.ts b/src/common/decorators/docs/common.decorator.ts similarity index 63% rename from src/decorators/docs/common.decorator.ts rename to src/common/decorators/docs/common.decorator.ts index 490119a..0b69dbe 100644 --- a/src/decorators/docs/common.decorator.ts +++ b/src/common/decorators/docs/common.decorator.ts @@ -1,11 +1,7 @@ +import { PageResponse } from '@/common/dtos/pageResponse'; import { applyDecorators } from '@nestjs/common'; -import { - ApiDefaultResponse, - ApiNotAcceptableResponse, - ApiQuery, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; -import { PageResponse } from 'src/interfaces/pageResponse'; +import { ApiDefaultResponse, ApiQuery } from '@nestjs/swagger'; +import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; export function ApiPagination() { return applyDecorators( @@ -28,9 +24,6 @@ export function ApiPagination() { '기본 page response. records에 data가 들어갑니다. 위의 type을 확인해주세요', type: PageResponse, }), - ApiNotAcceptableResponse({ - description: 'Invalid page query', - type: DocumentedException, - }), + ApiKudogExceptionResponse(['INVALID_PAGE_QUERY']), ); } diff --git a/src/common/decorators/docs/index.ts b/src/common/decorators/docs/index.ts new file mode 100644 index 0000000..e380fe4 --- /dev/null +++ b/src/common/decorators/docs/index.ts @@ -0,0 +1,8 @@ +export { AuthDocs } from './auth.decorator'; +export { CategoryDocs } from './category.decorator'; +export { MailDocs } from './mail.decorator'; +export { NoticeDocs } from './notice.decorator'; +export { NotificationDocs } from './notification.decorator'; +export { ScrapDocs } from './scrap.decorator'; +export { SubscribeDocs } from './subscribe.decorator'; +export { UserDocs } from './user.decorator'; diff --git a/src/common/decorators/docs/mail.decorator.ts b/src/common/decorators/docs/mail.decorator.ts new file mode 100644 index 0000000..2452838 --- /dev/null +++ b/src/common/decorators/docs/mail.decorator.ts @@ -0,0 +1,46 @@ +import type { MethodNames } from '@/common/types/method'; +import type { MailController } from '@/domain/mail/mail.controller'; +import { ApiCreatedResponse, ApiOperation } from '@nestjs/swagger'; +import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; + +type MailEndpoints = MethodNames; + +const MailDocsMap: Record = { + sendVerifyMail: [ + ApiOperation({ + summary: '인증 메일 전송', + description: + 'korea.ac.kr 메일을 인증합니다. 10초 내에 재요청 시, 이미 인증된 메일을 요청할 시 에러가 발생합니다.', + }), + ApiCreatedResponse({ description: '메일 전송 성공' }), + ApiKudogExceptionResponse([ + 'EMAIL_ALREADY_USED', + 'TOO_MANY_REQUESTS', + 'EMAIL_NOT_IN_KOREA_DOMAIN', + 'TODO_INVALID', + ]), + ], + checkVerifyCode: [ + ApiOperation({ + summary: '인증 코드 확인', + description: '인증 코드를 확인합니다.', + }), + ApiCreatedResponse({ + description: '인증 성공', + }), + ], +}; +export function MailDocs(target) { + for (const key in MailDocsMap) { + const methodDecorators = MailDocsMap[key as keyof typeof MailDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/common/decorators/docs/notice.decorator.ts b/src/common/decorators/docs/notice.decorator.ts new file mode 100644 index 0000000..23e8f4e --- /dev/null +++ b/src/common/decorators/docs/notice.decorator.ts @@ -0,0 +1,136 @@ +import type { MethodNames } from '@/common/types/method'; +import type { NoticeController } from '@/domain/notice/notice.controller'; +import { + ApiBody, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +import { AddRequestRequestDto } from 'src/domain/notice/dtos/AddRequestRequest.dto'; +import { NoticeInfoResponseDto } from 'src/domain/notice/dtos/NoticeInfoResponse.dto'; +import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; +import { ApiPagination } from './common.decorator'; + +type NoticeEndpoints = MethodNames; + +const NoticeDocsMap: Record = { + getNoticeList: [ + ApiOperation({ + summary: '공지 리스트 조회 by filter', + description: + 'database의 공지사항들을 작성날짜순으로 필터링하여 가져옵니다. 필터와 관련된 정보들은 쿼리 스트링으로, page size 10, Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiOkResponse({ + description: 'well done', + type: NoticeListResponseDto, + }), + ApiPagination(), + ApiQuery({ + name: 'keyword', + type: String, + required: false, + example: '장학', + description: '검색어를 입력', + }), + ApiQuery({ + name: 'providers', + type: String, + required: false, + description: + '필터를 적용할 학부 목록을 ","로 연결하여 띄어쓰기없이 작성해주세요', + example: '정보대학,미디어학부', + }), + ApiQuery({ + name: 'categories', + type: String, + required: false, + description: + '필터를 적용할 카테고리 목록을 ","로 연결하여 띄어쓰기없이 작성해주세요.', + example: '공지사항,장학정보', + }), + ApiQuery({ + name: 'start_date', + type: String, + required: false, + description: 'start date', + example: '2024-01-01', + }), + ApiQuery({ + name: 'end_date', + type: String, + required: false, + description: 'end date', + example: '2024-01-01', + }), + ], + getNoticeInfoById: [ + ApiOperation({ + summary: '공지사항 상세 조회', + description: + '공지사항의 상세 정보를 조회합니다. 조회수를 1 올립니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiOkResponse({ + description: 'well done', + type: NoticeInfoResponseDto, + }), + ApiParam({ + name: 'id', + type: Number, + description: 'notice id', + example: 1, + required: true, + }), + ], + scrapNotice: [ + ApiOperation({ + summary: '공지 스크랩', + description: + '공지사항을 스크랩합니다. 이미 스크랩되어있다면 취소합니다. 스크랩 시 true, 취소 시 false를 반환합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiParam({ + name: 'noticeId', + type: Number, + description: 'notice id', + example: 1, + required: true, + }), + ApiParam({ + name: 'scrapBoxId', + type: Number, + description: 'scrapBox id', + example: 1, + required: true, + }), + ApiOkResponse({ + description: '스크랩 성공 시 true, 스크랩 취소 시 false를 반환합니다.', + type: Boolean, + }), + ], + addNoticeRequest: [ + ApiOperation({ + description: '공지사항 추가 요청, access token 보내주세요', + summary: '공지사항 추가 요청', + }), + ApiBody({ + type: AddRequestRequestDto, + }), + ApiCreatedResponse({ description: 'OK' }), + ], +}; + +export function NoticeDocs(target) { + for (const key in NoticeDocsMap) { + const methodDecorators = NoticeDocsMap[key as keyof typeof NoticeDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/common/decorators/docs/notification.decorator.ts b/src/common/decorators/docs/notification.decorator.ts new file mode 100644 index 0000000..18ed4e9 --- /dev/null +++ b/src/common/decorators/docs/notification.decorator.ts @@ -0,0 +1,99 @@ +import type { MethodNames } from '@/common/types/method'; +import type { NotificationController } from '@/domain/notification/notification.controller'; +import { + ApiBody, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiQuery, +} from '@nestjs/swagger'; +import { NotificationInfoResponseDto } from 'src/domain/notification/dtos/noticiationInfoResponse.dto'; +import { TokenRequestDto } from 'src/domain/notification/dtos/tokenRequest.dto'; +import { ApiPagination } from './common.decorator'; + +type NotificationEndPoints = MethodNames; + +const NotificationDocsMap: Record = { + getNotifications: [ + ApiOperation({ + summary: '알림 내역 조회', + description: + '유저에게 지금까지 전송된 알림 내역을 제공합니다. Authorization : Bearer ${JWT}, 페이지네이션 가능합니다.', + }), + ApiPagination(), + ApiOkResponse({ + type: NotificationInfoResponseDto, + }), + ], + getNewNotifications: [ + ApiOperation({ + summary: '알림 내역 조회', + description: + '유저에게 새롭게 전송된 알림 내역을 제공합니다. 메인화면에 구독함 새로 올라갔어요! 알림에 쓰면 될듯해요 Authorization : Bearer ${JWT}, 페이지네이션 가능합니다.', + }), + ApiPagination(), + ApiOkResponse({ + type: NotificationInfoResponseDto, + }), + ], + registerToken: [ + ApiOperation({ + summary: 'FCM Token 등록', + description: + 'FCM Token을 등록합니다. Authorization : Bearer ${JWT}, getTokenStatus 호출 후, token이 등록되어있지 않을 때 사용해주세요.', + }), + ApiBody({ + type: TokenRequestDto, + }), + ApiCreatedResponse({ + description: 'Token 등록 성공', + }), + ], + deleteToken: [ + ApiOperation({ + summary: 'FCM Token 등록', + description: 'FCM Token을 삭제합니다. Authorization : Bearer ${JWT}', + }), + ApiBody({ + type: TokenRequestDto, + }), + ApiOkResponse({ + description: 'Token 등록 성공', + }), + ], + getTokenStatus: [ + ApiOperation({ + summary: 'FCM Token 상태 조회', + description: + '클라이언트의 Token 상태를 제공합니다. Authorization : Bearer ${JWT}, Token이 만료될 때도 있다고 합니다. Token이 만료되면 서버에서 자동으로 삭제하니, getTokenStatus로 체크하고, false 시 token을 새로 발급하여 등록해주세요.', + }), + ApiQuery({ + name: 'token', + type: String, + required: true, + }), + ApiOkResponse({ + type: Boolean, + description: '비활성화시 false, 등록 시 true', + }), + ], + sendNotification: [ + ApiOperation({ deprecated: true, summary: '알림 전송 test' }), + ], +}; + +export function NotificationDocs(target) { + for (const key in NotificationDocsMap) { + const methodDecorators = + NotificationDocsMap[key as keyof typeof NotificationDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/common/decorators/docs/scrap.decorator.ts b/src/common/decorators/docs/scrap.decorator.ts new file mode 100644 index 0000000..e77d597 --- /dev/null +++ b/src/common/decorators/docs/scrap.decorator.ts @@ -0,0 +1,119 @@ +import type { MethodNames } from '@/common/types/method'; +import type { ScrapController } from '@/domain/scrap/scrap.controller'; +import { + ApiBody, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, +} from '@nestjs/swagger'; +import { ScrapBoxRequestDto } from 'src/domain/scrap/dtos/scrapBoxRequest.dto'; +import { ScrapBoxResponseDto } from 'src/domain/scrap/dtos/scrapBoxResponse.dto'; +import { ScrapBoxResponseWithNotices } from 'src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto'; +import { ApiPagination } from './common.decorator'; + +type ScrapEndPoints = MethodNames; + +const ScrapDocsMap: Record = { + createScrapBox: [ + ApiOperation({ + summary: '스크랩박스 생성', + description: + '사용자를 위한 새 스크랩박스를 생성합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiBody({ + description: '스크랩박스 정보', + type: ScrapBoxRequestDto, + }), + ApiCreatedResponse({ + description: + '스크랩박스가 성공적으로 생성되었습니다. 만들어진 박스 정보를 반환합니다.', + type: ScrapBoxResponseDto, + }), + ], + getScrapBoxInfo: [ + ApiOperation({ + summary: + '스크랩 박스 세부 정보 열람. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + description: + '스크랩 박스 정보 + 스크랩 박스에 포함된 공지사항들을 반환합니다.', + }), + ApiParam({ + name: 'scrapBoxId', + description: '열람할 스크랩 박스의 id', + type: Number, + required: true, + example: 1, + }), + ApiOkResponse({ + description: '스크랩박스 정보 + 스크랩박스에 포함된 공지사항들', + type: ScrapBoxResponseWithNotices, + }), + ], + getScrapBoxes: [ + ApiOperation({ + summary: '스크랩박스 목록 조회', + description: + '사용자의 스크랩박스 목록을 조회합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiPagination(), + ApiOkResponse({ + description: '사용자의 스크랩박스 목록', + type: [ScrapBoxResponseDto], + }), + ], + updateScrapBox: [ + ApiOperation({ + summary: '스크랩 박스 정보 수정', + description: + '스크랩 박스의 정보를 수정합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiParam({ + name: 'scrapBoxId', + description: '수정할 스크랩 박스의 id', + type: Number, + required: true, + example: 1, + }), + ApiBody({ + description: '정보들', + type: ScrapBoxRequestDto, + }), + ApiOkResponse({ + description: '스크랩박스 수정 성공, 변경된 박스의 정보가 반환됩니다.', + type: ScrapBoxResponseDto, + }), + ], + deleteScrapBox: [ + ApiOperation({ + summary: '스크랩 박스 삭제', + description: + 'id에 맞는 스크랩 박스를 삭제합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiParam({ + name: 'scrapBoxId', + description: '삭제할 스크랩 박스의 id', + type: Number, + required: true, + example: 1, + }), + ApiOkResponse({ + description: '스크랩박스 삭제 성공', + }), + ], +}; + +export function ScrapDocs(target) { + for (const key in ScrapDocsMap) { + const methodDecorators = ScrapDocsMap[key as keyof typeof ScrapDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/common/decorators/docs/subscribe.decorator.ts b/src/common/decorators/docs/subscribe.decorator.ts new file mode 100644 index 0000000..789ef4c --- /dev/null +++ b/src/common/decorators/docs/subscribe.decorator.ts @@ -0,0 +1,152 @@ +import type { MethodNames } from '@/common/types/method'; +import type { SubscribeController } from '@/domain/subscribe/subscribe.controller'; +import { + ApiBody, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; +import { SubscribeBoxRequestDto } from 'src/domain/subscribe/dtos/subscribeBoxRequest.dto'; +import { SubscribeBoxResponseDto } from 'src/domain/subscribe/dtos/subscribeBoxResponse.dto'; +import { SubscribeBoxResponseDtoWithNotices } from 'src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto'; +import { ApiPagination } from './common.decorator'; + +type SubscribeEndPoint = MethodNames; + +const SubscribeDocsMap: Record = { + createSubscribeBox: [ + ApiOperation({ + summary: '구독함 생성', + description: + '새 구독함를 생성합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiBody({ + description: '구독함 정보', + type: SubscribeBoxRequestDto, + }), + ApiCreatedResponse({ + description: + '구독함이 성공적으로 생성되었습니다. 만들어진 박스 정보를 반환합니다.', + type: SubscribeBoxResponseDto, + }), + ], + getSubscribeInfo: [ + ApiOperation({ + summary: '구독함 날짜별 세부 정보 열람', + description: + '구독함 정보 + 구독함에 포함된 해당 날짜의 공지사항들을 반환합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiParam({ + name: 'subscribeBoxId', + description: '열람할 구독함의 id', + type: Number, + required: true, + example: 1, + }), + ApiQuery({ + name: 'date', + description: '열람할 날짜', + type: String, + required: true, + example: '2024-04-08', + }), + ApiOkResponse({ + description: '구독함 정보 + 구독함에 포함된 공지사항들', + type: SubscribeBoxResponseDtoWithNotices, + }), + ], + getSubscribeBoxes: [ + ApiOperation({ + summary: '구독함 목록 조회', + description: + '사용자의 구독함 목록을 조회합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiPagination(), + ApiOkResponse({ + description: '사용자의 구독함 목록', + type: [SubscribeBoxResponseDto], + }), + ], + updateSubscribeBox: [ + ApiOperation({ + summary: '구독함 정보 수정', + description: + '구독함의 정보를 수정합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiParam({ + name: 'scrapBoxId', + description: '수정할 구독함의 id', + type: Number, + required: true, + example: 1, + }), + ApiBody({ + description: '구독함 정보들', + type: SubscribeBoxRequestDto, + }), + ApiOkResponse({ + description: '구독함 수정 성공, 변경된 구독함의 정보가 반환됩니다.', + type: SubscribeBoxResponseDto, + }), + ], + deleteSubscribeBox: [ + ApiOperation({ + summary: '구독함 삭제', + description: + 'id에 맞는 구독함을 삭제합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiParam({ + name: 'subscribeBoxId', + description: '구독함의 id', + type: Number, + required: true, + example: 1, + }), + ApiOkResponse({ + description: '구독함 삭제 성공', + }), + ], + getNoticesByBoxWithDate: [ + ApiOperation({ + summary: '구독함 날짜별 공지사항 조회', + description: + '구독함에 포함된 해당 날짜의 공지사항들을 반환합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiParam({ + name: 'subscribeBoxId', + description: '조회할 구독함의 id', + type: Number, + required: true, + example: 1, + }), + ApiQuery({ + name: 'date', + description: '조회할 날짜', + type: String, + required: true, + example: '2024. 04. 08', + }), + ApiOkResponse({ + type: [NoticeListResponseDto], + }), + ], +}; + +export function SubscribeDocs(target) { + for (const key in SubscribeDocsMap) { + const methodDecorators = + SubscribeDocsMap[key as keyof typeof SubscribeDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/common/decorators/docs/user.decorator.ts b/src/common/decorators/docs/user.decorator.ts new file mode 100644 index 0000000..f7fefda --- /dev/null +++ b/src/common/decorators/docs/user.decorator.ts @@ -0,0 +1,43 @@ +import type { MethodNames } from '@/common/types/method'; +import type { UsersController } from '@/domain/users/users.controller'; +import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; +import { UserInfoResponseDto } from 'src/domain/users/dtos/userInfo.dto'; + +type UserEndpoints = MethodNames; + +const UserDocsMap: Record = { + getUserInfo: [ + ApiOperation({ + summary: 'GET user info', + description: + 'access token을 이용하여 내 정보를 가져옵니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', + }), + ApiOkResponse({ + description: '내 정보 get', + type: UserInfoResponseDto, + }), + ], + modifyUserInfo: [ + ApiOperation({ + summary: 'modify user info', + description: + '내 정보를 수정합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요. 수정할 정보만 보내도 괜찮습니다.', + }), + ApiOkResponse({ description: '정보 수정 성공' }), + ], +}; + +export function UserDocs(target) { + for (const key in UserDocsMap) { + const methodDecorators = UserDocsMap[key as keyof typeof UserDocsMap]; + + const descriptor = Object.getOwnPropertyDescriptor(target.prototype, key); + if (descriptor) { + for (const decorator of methodDecorators) { + decorator(target.prototype, key, descriptor); + } + Object.defineProperty(target.prototype, key, descriptor); + } + } + return target; +} diff --git a/src/decorators/index.ts b/src/common/decorators/index.ts similarity index 83% rename from src/decorators/index.ts rename to src/common/decorators/index.ts index 36e8c8d..5026273 100644 --- a/src/decorators/index.ts +++ b/src/common/decorators/index.ts @@ -2,3 +2,4 @@ export * from './injectAccessUser.decorator'; export * from './usePagination.decorator'; export * from './injectLocalUser.decorator'; export * from './injectRefreshUser.decorator'; +export * from './namedController'; diff --git a/src/decorators/injectAccessUser.decorator.ts b/src/common/decorators/injectAccessUser.decorator.ts similarity index 84% rename from src/decorators/injectAccessUser.decorator.ts rename to src/common/decorators/injectAccessUser.decorator.ts index b473796..d73216b 100644 --- a/src/decorators/injectAccessUser.decorator.ts +++ b/src/common/decorators/injectAccessUser.decorator.ts @@ -1,5 +1,5 @@ +import { JwtPayload } from '@/common/types/auth'; import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -import { JwtPayload } from 'src/interfaces/auth'; export const InjectAccessUser = createParamDecorator( (_: unknown, cts: ExecutionContext): JwtPayload => { diff --git a/src/decorators/injectLocalUser.decorator.ts b/src/common/decorators/injectLocalUser.decorator.ts similarity index 100% rename from src/decorators/injectLocalUser.decorator.ts rename to src/common/decorators/injectLocalUser.decorator.ts diff --git a/src/decorators/injectRefreshUser.decorator.ts b/src/common/decorators/injectRefreshUser.decorator.ts similarity index 82% rename from src/decorators/injectRefreshUser.decorator.ts rename to src/common/decorators/injectRefreshUser.decorator.ts index 60ec28b..dfb3d60 100644 --- a/src/decorators/injectRefreshUser.decorator.ts +++ b/src/common/decorators/injectRefreshUser.decorator.ts @@ -1,5 +1,5 @@ +import { RefreshTokenPayload } from '@/common/types/auth'; import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -import { RefreshTokenPayload } from 'src/interfaces/auth'; export const InjectRefreshUser = createParamDecorator( (_: unknown, cts: ExecutionContext): RefreshTokenPayload => { diff --git a/src/common/decorators/namedController.ts b/src/common/decorators/namedController.ts new file mode 100644 index 0000000..291555b --- /dev/null +++ b/src/common/decorators/namedController.ts @@ -0,0 +1,6 @@ +import { Controller, applyDecorators } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +export function NamedController(name: string) { + return applyDecorators(ApiTags(name), Controller(name)); +} diff --git a/src/decorators/usePagination.decorator.ts b/src/common/decorators/usePagination.decorator.ts similarity index 79% rename from src/decorators/usePagination.decorator.ts rename to src/common/decorators/usePagination.decorator.ts index 0647e77..7cf7024 100644 --- a/src/decorators/usePagination.decorator.ts +++ b/src/common/decorators/usePagination.decorator.ts @@ -1,10 +1,10 @@ +import { PageQuery } from '@/common/dtos/pageQuery'; import { ExecutionContext, NotAcceptableException, createParamDecorator, } from '@nestjs/common'; import { Request } from 'express'; -import { PageQuery } from 'src/interfaces/pageQuery'; export const UsePagination = createParamDecorator( (_: unknown, cts: ExecutionContext): PageQuery => { @@ -14,8 +14,8 @@ export const UsePagination = createParamDecorator( typeof request.query.pageSize !== 'string' ) throw new NotAcceptableException('Invalid page query'); - const page = parseInt(request.query.page, 10); - const pageSize = parseInt(request.query.pageSize, 10); + const page = Number.parseInt(request.query.page, 10); + const pageSize = Number.parseInt(request.query.pageSize, 10); if (isNaN(page) || isNaN(pageSize) || page < 1 || pageSize < 1) throw new NotAcceptableException('Invalid page query'); diff --git a/src/interfaces/pageQuery.ts b/src/common/dtos/pageQuery.ts similarity index 100% rename from src/interfaces/pageQuery.ts rename to src/common/dtos/pageQuery.ts diff --git a/src/interfaces/pageResponse.ts b/src/common/dtos/pageResponse.ts similarity index 100% rename from src/interfaces/pageResponse.ts rename to src/common/dtos/pageResponse.ts diff --git a/src/interfaces/auth.ts b/src/common/types/auth.ts similarity index 100% rename from src/interfaces/auth.ts rename to src/common/types/auth.ts diff --git a/src/common/types/method.ts b/src/common/types/method.ts new file mode 100644 index 0000000..435e467 --- /dev/null +++ b/src/common/types/method.ts @@ -0,0 +1,4 @@ +export type MethodNames = { + [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; +}[keyof T]; + diff --git a/src/utils/date.ts b/src/common/utils/date.ts similarity index 100% rename from src/utils/date.ts rename to src/common/utils/date.ts diff --git a/src/common/utils/exception.ts b/src/common/utils/exception.ts new file mode 100644 index 0000000..b1f0575 --- /dev/null +++ b/src/common/utils/exception.ts @@ -0,0 +1,29 @@ +import * as ERROR from '@/asset/error.json'; + +interface KudogErrorResponse { + statusCode: number; + errorCode: number; + message: string; + name: string; +} + +export type ExceptionNames = keyof typeof ERROR; + +export const EXCEPTIONS: { [key in ExceptionNames]: KudogErrorResponse } = + ERROR; + +export class HttpException extends Error { + statusCode: number; + errorCode: number; + constructor(name: ExceptionNames) { + super(name); + this.message = EXCEPTIONS[name].message; + this.name = name; + this.statusCode = EXCEPTIONS[name].statusCode; + this.errorCode = EXCEPTIONS[name].errorCode; + } +} + +export function throwKudogException(name: ExceptionNames) { + throw new HttpException(name); +} diff --git a/src/decorators/docs/auth.decorator.ts b/src/decorators/docs/auth.decorator.ts deleted file mode 100644 index ff19ec6..0000000 --- a/src/decorators/docs/auth.decorator.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiBody, - ApiCreatedResponse, - ApiHeader, - ApiNotAcceptableResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiRequestTimeoutResponse, - ApiResponse, - ApiTooManyRequestsResponse, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { LoginRequestDto } from 'src/domain/auth/dtos/loginRequestDto'; -import { TokenResponseDto } from 'src/domain/auth/dtos/tokenResponse.dto'; -import { DocumentedException } from 'src/interfaces/docsException'; - -type AuthEndpoints = - | 'login' - | 'signup' - | 'refresh' - | 'logout' - | 'deleteUser' - | 'changePwdRequest' - | 'verifyChangePwdCode' - | 'changePassword'; - -export function Docs(endPoint: AuthEndpoints) { - switch (endPoint) { - case 'login': - return applyDecorators( - ApiOperation({ - description: - 'portal email, password를 통해 로그인, access, refresh JWT 발급', - summary: '로그인', - }), - ApiBody({ - type: LoginRequestDto, - description: 'portal email을 이용하여 로그인합니다.', - }), - ApiCreatedResponse({ - description: '로그인 성공', - type: TokenResponseDto, - }), - ApiUnauthorizedResponse({ - description: '로그인 실패', - type: DocumentedException, - }), - ); - case 'signup': - return applyDecorators( - ApiOperation({ - description: '회원가입', - summary: '회원가입', - }), - ApiCreatedResponse({ - description: '회원가입 성공 ', - type: TokenResponseDto, - }), - ApiBadRequestResponse({ - description: - 'message : 인증 후 너무 오랜 시간이 지났습니다. 다시 인증해주세요. |인증되지 않은 이메일입니다. | 사용중인 이메일입니다.| korea.ac.kr 이메일이 아닙니다. | 비밀번호는 6~16자의 영문 소문자와 숫자로만 입력해주세요.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'refresh': - return applyDecorators( - ApiHeader({ - description: - 'Authorization header에 Bearer token 형태로 refresh token을 넣어주세요.', - name: 'authorization', - required: true, - }), - ApiOperation({ - description: - 'refresh token을 통해 access token 재발급, refresh token도 회전됩니다.', - summary: '토큰 재발급', - }), - ApiCreatedResponse({ - description: '토큰 재발급 성공', - type: TokenResponseDto, - }), - ApiUnauthorizedResponse({ - description: '토큰 재발급 실패, 세부 사항은 에러 메시지에 있습니다.', - type: DocumentedException, - }), - ); - case 'logout': - return applyDecorators( - ApiOperation({ - summary: '로그아웃', - description: - 'refresh token을 삭제합니다. storage에서 두 토큰을 삭제해주세요. authorization header에 Bearer ${accessToken} 을 담아주세요.', - }), - ApiHeader({ - description: - 'Authorization header에 Bearer token 형태로 refresh token을 넣어주세요.', - name: 'authorization', - required: true, - }), - ApiUnauthorizedResponse({ - description: 'invalid access token', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: 'access token의 id값이 invalid', - type: DocumentedException, - }), - ApiOkResponse({ - description: 'logout 성공', - }), - ); - case 'deleteUser': - return applyDecorators( - ApiOperation({ - summary: '회원 탈퇴', - description: - '회원 탈퇴합니다. authorization header에 Bearer ${accessToken} 을 담아주세요.', - }), - ApiUnauthorizedResponse({ - description: 'invalid access token', - type: DocumentedException, - }), - ApiOkResponse({ - description: '회원 탈퇴 성공', - }), - ); - case 'changePwdRequest': - return applyDecorators( - ApiOperation({ - summary: '비밀번호 변경 이메일 인증 요청', - description: '비밀번호를 변경하기 위하여 이메일 인증을 요청합니다.', - }), - ApiCreatedResponse({ - description: '이메일 전송 성공. 3분 안에 인증 코드를 입력해주세요.', - }), - ApiNotFoundResponse({ - description: '해당 이메일의 유저가 존재하지 않습니다.', - type: DocumentedException, - }), - ApiTooManyRequestsResponse({ - description: '잠시 후에 다시 시도해주세요. (10초 내 재요청)', - type: DocumentedException, - }), - ApiBadRequestResponse({ - description: 'korea.ac.kr 이메일을 입력하세요.', - type: DocumentedException, - }), - ApiResponse({ - status: 510, - description: - '알 수 없는 이유로 메일 전송에 실패했습니다. 잠시 후에 다시 시도해주세요.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'verifyChangePwdCode': - return applyDecorators( - ApiOperation({ - summary: '비밀번호 변경 인증 코드 확인', - description: - '이메일로 전송된 인증 코드를 확인합니다. 제한시간은 3분이며, 인증 이후 10분 안에 비밀번호를 변경할 수 있습니다.', - }), - ApiCreatedResponse({ - description: '인증 성공, 비밀번호를 10분간 변경할 수 있습니다.', - }), - ApiBadRequestResponse({ - description: '인증 코드가 일치하지 않습니다.', - type: DocumentedException, - }), - ApiRequestTimeoutResponse({ - description: - '인증 요청 이후 3분이 지났습니다. 다시 메일 전송을 해주세요.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'changePassword': - return applyDecorators( - ApiOperation({ - summary: '비밀번호 변경', - description: - '비밀번호를 변경합니다. 인증 코드 확인 이후 10분 안에 비밀번호를 변경해주세요.', - }), - ApiOkResponse({ description: '비밀번호 변경 성공' }), - ApiNotFoundResponse({ - description: '존재하지 않는 유저입니다.', - type: DocumentedException, - }), - ApiUnauthorizedResponse({ - description: '인증 코드 인증이 완료되지 않았습니다.', - type: DocumentedException, - }), - ApiRequestTimeoutResponse({ - description: - '인증 이후 10분이 지났습니다. 다시 메일 전송을 해주세요.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/category.decorator.ts b/src/decorators/docs/category.decorator.ts deleted file mode 100644 index c11af64..0000000 --- a/src/decorators/docs/category.decorator.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiNotAcceptableResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { ProviderListResponseDto } from 'src/domain/category/dtos/ProviderListResponse.dto'; -import { CategoryListResponseDto } from 'src/domain/category/dtos/categoryListResponse.dto'; -import { DocumentedException } from 'src/interfaces/docsException'; - -type CategoryEndpoints = - | 'getProviders' - | 'getCategories' - | 'getBookmarkedProviders'; - -export function Docs(endPoint: CategoryEndpoints) { - switch (endPoint) { - case 'getProviders': - return applyDecorators( - ApiOperation({ - summary: '학부 리스트 조회', - description: - 'DB의 학부 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiOkResponse({ - description: '학부 리스트', - type: [ProviderListResponseDto], - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ); - case 'getCategories': - return applyDecorators( - ApiOperation({ - summary: '학부 소속 카테고리 리스트 조회', - description: - 'DB의 학부 소속 카테고리 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiParam({ - name: 'id', - description: '학부 id', - type: Number, - required: true, - example: 1, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ApiOkResponse({ - description: '스크랩학부 소속 카테고리들', - type: [CategoryListResponseDto], - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ); - case 'getBookmarkedProviders': - return applyDecorators( - ApiOperation({ - summary: '즐겨찾는 학부 목록 조회', - description: - '유저의 즐겨찾는 학과 목록 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiOkResponse({ - description: '사용자의 즐겨찾는 학과 목록', - type: [ProviderListResponseDto], - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/index.ts b/src/decorators/docs/index.ts deleted file mode 100644 index dff1ab5..0000000 --- a/src/decorators/docs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './common.decorator'; diff --git a/src/decorators/docs/mail.decorator.ts b/src/decorators/docs/mail.decorator.ts deleted file mode 100644 index cd7021d..0000000 --- a/src/decorators/docs/mail.decorator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiConflictResponse, - ApiCreatedResponse, - ApiNotAcceptableResponse, - ApiNotFoundResponse, - ApiOperation, - ApiRequestTimeoutResponse, - ApiTooManyRequestsResponse, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; - -type MailEndpoints = 'sendVerifyMail' | 'checkVerifyCode'; - -export function Docs(endPoint: MailEndpoints) { - switch (endPoint) { - case 'sendVerifyMail': - return applyDecorators( - ApiOperation({ - summary: '인증 메일 전송', - description: - 'korea.ac.kr 메일을 인증합니다. 10초 내에 재요청 시, 이미 인증된 메일을 요청할 시 에러가 발생합니다.', - }), - ApiCreatedResponse({ description: '메일 전송 성공' }), - ApiConflictResponse({ - description: '사용중인 메일', - type: DocumentedException, - }), - ApiTooManyRequestsResponse({ - description: '10초 내 재요청', - type: DocumentedException, - }), - ApiBadRequestResponse({ - description: 'korea.ac.kr 메일이 아님', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'checkVerifyCode': - return applyDecorators( - ApiOperation({ - summary: '인증 코드 확인', - description: '인증 코드를 확인합니다.', - }), - ApiCreatedResponse({ - description: '인증 성공', - type: DocumentedException, - }), - ApiConflictResponse({ - description: '이미 인증된 메일', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '해당 메일이 존재하지 않음', - type: DocumentedException, - }), - ApiBadRequestResponse({ - description: '인증 코드 불일치', - type: DocumentedException, - }), - ApiRequestTimeoutResponse({ - description: '요청한지 3분 경과', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/notice.decorator.ts b/src/decorators/docs/notice.decorator.ts deleted file mode 100644 index b2fc367..0000000 --- a/src/decorators/docs/notice.decorator.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiBody, - ApiCreatedResponse, - ApiForbiddenResponse, - ApiInternalServerErrorResponse, - ApiNotAcceptableResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; -import { NoticeInfoResponseDto } from 'src/domain/notice/dtos/NoticeInfoResponse.dto'; -import { ApiPagination } from './common.decorator'; -import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; -import { AddRequestRequestDto } from 'src/domain/notice/dtos/AddRequestRequest.dto'; - -type NoticeEndpoints = - | 'getNoticeList' - | 'getNoticeInfoById' - | 'scrapNotice' - | 'addNoticeRequest'; - -export function Docs(endPoint: NoticeEndpoints) { - switch (endPoint) { - case 'getNoticeList': - return applyDecorators( - ApiOperation({ - summary: '공지 리스트 조회 by filter', - description: - 'database의 공지사항들을 작성날짜순으로 필터링하여 가져옵니다. 필터와 관련된 정보들은 쿼리 스트링으로, page size 10, Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiOkResponse({ - description: 'well done', - type: NoticeListResponseDto, - }), - ApiInternalServerErrorResponse({ - description: 'internal server error', - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ApiPagination(), - ApiQuery({ - name: 'keyword', - type: String, - required: false, - example: '장학', - description: '검색어를 입력', - }), - ApiQuery({ - name: 'providers', - type: String, - required: false, - description: - '필터를 적용할 학부 목록을 ","로 연결하여 띄어쓰기없이 작성해주세요', - example: '정보대학,미디어학부', - }), - ApiQuery({ - name: 'categories', - type: String, - required: false, - description: - '필터를 적용할 카테고리 목록을 ","로 연결하여 띄어쓰기없이 작성해주세요.', - example: '공지사항,장학정보', - }), - ApiQuery({ - name: 'start_date', - type: String, - required: false, - description: 'start date', - example: '2024-01-01', - }), - ApiQuery({ - name: 'end_date', - type: String, - required: false, - description: 'end date', - example: '2024-01-01', - }), - ); - case 'getNoticeInfoById': - return applyDecorators( - ApiOperation({ - summary: '공지사항 상세 조회', - description: - '공지사항의 상세 정보를 조회합니다. 조회수를 1 올립니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiOkResponse({ - description: 'well done', - type: NoticeInfoResponseDto, - }), - ApiInternalServerErrorResponse({ - description: 'internal server error', - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ApiParam({ - name: 'id', - type: Number, - description: 'notice id', - example: 1, - required: true, - }), - ); - case 'scrapNotice': - return applyDecorators( - ApiOperation({ - summary: '공지 스크랩', - description: - '공지사항을 스크랩합니다. 이미 스크랩되어있다면 취소합니다. 스크랩 시 true, 취소 시 false를 반환합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiParam({ - name: 'noticeId', - type: Number, - description: 'notice id', - example: 1, - required: true, - }), - ApiParam({ - name: 'scrapBoxId', - type: Number, - description: 'scrapBox id', - example: 1, - required: true, - }), - ApiOkResponse({ - description: - '스크랩 성공 시 true, 스크랩 취소 시 false를 반환합니다.', - type: Boolean, - }), - ApiForbiddenResponse({ - description: '스크랩함의 소유자가 아닙니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: - '해당 id의 scrapBox가 존재하지 않습니다. | 해당 id의 notice가 존재하지 않습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'addNoticeRequest': - return applyDecorators( - ApiOperation({ - description: '공지사항 추가 요청, access token 보내주세요', - summary: '공지사항 추가 요청', - }), - ApiBody({ - type: AddRequestRequestDto, - }), - ApiCreatedResponse({ description: 'OK' }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/notification.decorator.ts b/src/decorators/docs/notification.decorator.ts deleted file mode 100644 index b9b4055..0000000 --- a/src/decorators/docs/notification.decorator.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiBody, - ApiCreatedResponse, - ApiNotAcceptableResponse, - ApiOkResponse, - ApiOperation, - ApiQuery, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; -import { NotificationInfoResponseDto } from 'src/domain/notification/dtos/noticiationInfoResponse.dto'; -import { TokenRequestDto } from 'src/domain/notification/dtos/tokenRequest.dto'; -import { ApiPagination } from './common.decorator'; - -type NotificationEndPoints = - | 'getNotifications' - | 'getNewNotifications' - | 'registerToken' - | 'deleteToken' - | 'getTokenStatus'; - -export function Docs(endPoint: NotificationEndPoints) { - switch (endPoint) { - case 'getNotifications': - return applyDecorators( - ApiOperation({ - summary: '알림 내역 조회', - description: - '유저에게 지금까지 전송된 알림 내역을 제공합니다. Authorization : Bearer ${JWT}, 페이지네이션 가능합니다.', - }), - ApiPagination(), - ApiOkResponse({ - type: NotificationInfoResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getNewNotifications': - return applyDecorators( - ApiOperation({ - summary: '알림 내역 조회', - description: - '유저에게 새롭게 전송된 알림 내역을 제공합니다. 메인화면에 구독함 새로 올라갔어요! 알림에 쓰면 될듯해요 Authorization : Bearer ${JWT}, 페이지네이션 가능합니다.', - }), - ApiPagination(), - ApiOkResponse({ - type: NotificationInfoResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'registerToken': - return applyDecorators( - ApiOperation({ - summary: 'FCM Token 등록', - description: - 'FCM Token을 등록합니다. Authorization : Bearer ${JWT}, getTokenStatus 호출 후, token이 등록되어있지 않을 때 사용해주세요.', - }), - ApiBody({ - type: TokenRequestDto, - }), - ApiCreatedResponse({ - description: 'Token 등록 성공', - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'deleteToken': - return applyDecorators( - ApiOperation({ - summary: 'FCM Token 등록', - description: 'FCM Token을 삭제합니다. Authorization : Bearer ${JWT}', - }), - ApiBody({ - type: TokenRequestDto, - }), - ApiOkResponse({ - description: 'Token 등록 성공', - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getTokenStatus': - return applyDecorators( - ApiOperation({ - summary: 'FCM Token 상태 조회', - description: - '클라이언트의 Token 상태를 제공합니다. Authorization : Bearer ${JWT}, Token이 만료될 때도 있다고 합니다. Token이 만료되면 서버에서 자동으로 삭제하니, getTokenStatus로 체크하고, false 시 token을 새로 발급하여 등록해주세요.', - }), - ApiQuery({ - name: 'token', - type: String, - required: true, - }), - ApiOkResponse({ - type: Boolean, - description: '비활성화시 false, 등록 시 true', - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/scrap.decorator.ts b/src/decorators/docs/scrap.decorator.ts deleted file mode 100644 index 22095a0..0000000 --- a/src/decorators/docs/scrap.decorator.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiBody, - ApiCreatedResponse, - ApiForbiddenResponse, - ApiNotAcceptableResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; -import { ScrapBoxRequestDto } from 'src/domain/scrap/dtos/scrapBoxRequest.dto'; -import { ScrapBoxResponseDto } from 'src/domain/scrap/dtos/scrapBoxResponse.dto'; -import { ScrapBoxResponseWithNotices } from 'src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto'; -import { ApiPagination } from './common.decorator'; - -type ScrapEndPoints = - | 'createScrapBox' - | 'getScrapBoxInfo' - | 'getScrapBoxes' - | 'updateScrapBox' - | 'deleteScrapBox'; - -export function Docs(endPoint: ScrapEndPoints) { - switch (endPoint) { - case 'createScrapBox': - return applyDecorators( - ApiOperation({ - summary: '스크랩박스 생성', - description: - '사용자를 위한 새 스크랩박스를 생성합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiBody({ - description: '스크랩박스 정보', - type: ScrapBoxRequestDto, - }), - ApiCreatedResponse({ - description: - '스크랩박스가 성공적으로 생성되었습니다. 만들어진 박스 정보를 반환합니다.', - type: ScrapBoxResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getScrapBoxInfo': - return applyDecorators( - ApiOperation({ - summary: - '스크랩 박스 세부 정보 열람. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - description: - '스크랩 박스 정보 + 스크랩 박스에 포함된 공지사항들을 반환합니다.', - }), - ApiParam({ - name: 'scrapBoxId', - description: '열람할 스크랩 박스의 id', - type: Number, - required: true, - example: 1, - }), - ApiOkResponse({ - description: '스크랩박스 정보 + 스크랩박스에 포함된 공지사항들', - type: ScrapBoxResponseWithNotices, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 scrapBox 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: 'scrapBox Id에 해당하는 박스가 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getScrapBoxes': - return applyDecorators( - ApiOperation({ - summary: '스크랩박스 목록 조회', - description: - '사용자의 스크랩박스 목록을 조회합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiPagination(), - ApiOkResponse({ - description: '사용자의 스크랩박스 목록', - type: [ScrapBoxResponseDto], - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ); - case 'updateScrapBox': - return applyDecorators( - ApiOperation({ - summary: '스크랩 박스 정보 수정', - description: - '스크랩 박스의 정보를 수정합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiParam({ - name: 'scrapBoxId', - description: '수정할 스크랩 박스의 id', - type: Number, - required: true, - example: 1, - }), - ApiBody({ - description: '정보들', - type: ScrapBoxRequestDto, - }), - ApiOkResponse({ - description: '스크랩박스 수정 성공, 변경된 박스의 정보가 반환됩니다.', - type: ScrapBoxResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 scrapBox 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: 'scrapBox Id에 해당하는 박스가 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'deleteScrapBox': - return applyDecorators( - ApiOperation({ - summary: '스크랩 박스 삭제', - description: - 'id에 맞는 스크랩 박스를 삭제합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiParam({ - name: 'scrapBoxId', - description: '삭제할 스크랩 박스의 id', - type: Number, - required: true, - example: 1, - }), - ApiOkResponse({ - description: '스크랩박스 삭제 성공', - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 scrapBox 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: 'scrapBox Id에 해당하는 박스가 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/subscribe.decorator.ts b/src/decorators/docs/subscribe.decorator.ts deleted file mode 100644 index 8067ee3..0000000 --- a/src/decorators/docs/subscribe.decorator.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiBody, - ApiCreatedResponse, - ApiForbiddenResponse, - ApiNotAcceptableResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiParam, - ApiQuery, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; -import { SubscribeBoxRequestDto } from 'src/domain/subscribe/dtos/subscribeBoxRequest.dto'; -import { SubscribeBoxResponseDto } from 'src/domain/subscribe/dtos/subscribeBoxResponse.dto'; -import { SubscribeBoxResponseDtoWithNotices } from 'src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto'; -import { ApiPagination } from './common.decorator'; -import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; - -type SubscribeEndPoint = - | 'createSubscribeBox' - | 'getSubscribeBoxInfo' - | 'getSubscribeBoxes' - | 'updateSubscribeBox' - | 'deleteSubscribeBox' - | 'getNoticesByBoxWithDate'; - -export function Docs(endPoint: SubscribeEndPoint) { - switch (endPoint) { - case 'createSubscribeBox': - return applyDecorators( - ApiOperation({ - summary: '구독함 생성', - description: - '새 구독함를 생성합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiBody({ - description: '구독함 정보', - type: SubscribeBoxRequestDto, - }), - ApiCreatedResponse({ - description: - '구독함이 성공적으로 생성되었습니다. 만들어진 박스 정보를 반환합니다.', - type: SubscribeBoxResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getSubscribeBoxInfo': - return applyDecorators( - ApiOperation({ - summary: '구독함 날짜별 세부 정보 열람', - description: - '구독함 정보 + 구독함에 포함된 해당 날짜의 공지사항들을 반환합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiParam({ - name: 'subscribeBoxId', - description: '열람할 구독함의 id', - type: Number, - required: true, - example: 1, - }), - ApiQuery({ - name: 'date', - description: '열람할 날짜', - type: String, - required: true, - example: '2024-04-08', - }), - ApiOkResponse({ - description: '구독함 정보 + 구독함에 포함된 공지사항들', - type: SubscribeBoxResponseDtoWithNotices, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 구독함 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '구독함 Id에 해당하는 구독함이 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getSubscribeBoxes': - return applyDecorators( - ApiOperation({ - summary: '구독함 목록 조회', - description: - '사용자의 구독함 목록을 조회합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiPagination(), - ApiOkResponse({ - description: '사용자의 구독함 목록', - type: [SubscribeBoxResponseDto], - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ); - case 'updateSubscribeBox': - return applyDecorators( - ApiOperation({ - summary: '구독함 정보 수정', - description: - '구독함의 정보를 수정합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiParam({ - name: 'scrapBoxId', - description: '수정할 구독함의 id', - type: Number, - required: true, - example: 1, - }), - ApiBody({ - description: '구독함 정보들', - type: SubscribeBoxRequestDto, - }), - ApiOkResponse({ - description: '구독함 수정 성공, 변경된 구독함의 정보가 반환됩니다.', - type: SubscribeBoxResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 구독함 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '구독함 Id에 해당하는 구독함이 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'deleteSubscribeBox': - return applyDecorators( - ApiOperation({ - summary: '구독함 삭제', - description: - 'id에 맞는 구독함을 삭제합니다. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', - }), - ApiParam({ - name: 'subscribeBoxId', - description: '구독함의 id', - type: Number, - required: true, - example: 1, - }), - ApiOkResponse({ - description: '구독함 삭제 성공', - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 구독함 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '구독함 Id에 해당하는 구독함이 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - case 'getNoticesByBoxWithDate': - return applyDecorators( - ApiOperation({ - summary: '구독함 날짜별 공지사항 조회', - description: - '구독함에 포함된 해당 날짜의 공지사항들을 반환합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiParam({ - name: 'subscribeBoxId', - description: '조회할 구독함의 id', - type: Number, - required: true, - example: 1, - }), - ApiQuery({ - name: 'date', - description: '조회할 날짜', - type: String, - required: true, - example: '2024. 04. 08', - }), - ApiOkResponse({ - type: [NoticeListResponseDto], - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiForbiddenResponse({ - description: 'userId와 구독함 소유자의 id가 다릅니다.', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '구독함 Id에 해당하는 구독함이 없습니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/decorators/docs/user.decorator.ts b/src/decorators/docs/user.decorator.ts deleted file mode 100644 index 99dde95..0000000 --- a/src/decorators/docs/user.decorator.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { - ApiNotAcceptableResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, - ApiUnauthorizedResponse, -} from '@nestjs/swagger'; -import { DocumentedException } from 'src/interfaces/docsException'; -import { UserInfoResponseDto } from 'src/domain/users/dtos/userInfo.dto'; - -type UserEndpoints = 'getUserInfo' | 'modifyUserInfo'; - -export function Docs(endPoint: UserEndpoints) { - switch (endPoint) { - case 'getUserInfo': - return applyDecorators( - ApiOperation({ - summary: 'GET user info', - description: - 'access token을 이용하여 내 정보를 가져옵니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요.', - }), - ApiOkResponse({ - description: '내 정보 get', - type: UserInfoResponseDto, - }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '존재하지 않는 유저입니다.', - type: DocumentedException, - }), - ); - case 'modifyUserInfo': - return applyDecorators( - ApiOperation({ - summary: 'modify user info', - description: - '내 정보를 수정합니다. Authorization 헤더에 Bearer ${accessToken} 을 넣어주세요. 수정할 정보만 보내도 괜찮습니다.', - }), - ApiOkResponse({ description: '정보 수정 성공' }), - ApiUnauthorizedResponse({ - description: 'token 만료 또는 잘못된 token', - type: DocumentedException, - }), - ApiNotFoundResponse({ - description: '존재하지 않는 유저입니다.', - type: DocumentedException, - }), - ApiNotAcceptableResponse({ - description: '입력값이 유효하지 않습니다 - <변수명> 상세 정보', - type: DocumentedException, - }), - ); - } -} diff --git a/src/domain/auth/auth.controller.ts b/src/domain/auth/auth.controller.ts index 4fd74f6..b8d11b7 100644 --- a/src/domain/auth/auth.controller.ts +++ b/src/domain/auth/auth.controller.ts @@ -1,83 +1,77 @@ -import { Body, Controller, Delete, Post, Put, UseGuards } from '@nestjs/common'; +import { + InjectAccessUser, + InjectRefreshUser, + NamedController, + injectLocalUser, +} from '@/common/decorators'; +import { AuthDocs } from '@/common/decorators/docs'; +import { JwtPayload, RefreshTokenPayload } from '@/common/types/auth'; +import { Body, Delete, Post, Put } from '@nestjs/common'; import { AuthService } from './auth.service'; -import { SignupRequestDto } from './dtos/signupRequest.dto'; -import { AuthGuard } from '@nestjs/passport'; -import { ApiTags } from '@nestjs/swagger'; -import { TokenResponseDto } from './dtos/tokenResponse.dto'; import { ChangePasswordDto, ChangePasswordRequestDto, VerifyChangePasswordRequestDto, } from './dtos/changePwdRequest.dto'; -import { Docs } from 'src/decorators/docs/auth.decorator'; -import { - InjectAccessUser, - InjectRefreshUser, - injectLocalUser, -} from 'src/decorators'; -import { JwtPayload, RefreshTokenPayload } from 'src/interfaces/auth'; +import { SignupRequestDto } from './dtos/signupRequest.dto'; +import { TokenResponseDto } from './dtos/tokenResponse.dto'; +import { JwtAccessGuard } from './passport/accessToken.strategy'; +import { LocalGuard } from './passport/local.strategy'; +import { JwtRefreshGuard } from './passport/refreshToken.strategy'; -@Controller('auth') -@ApiTags('auth') +@AuthDocs +@NamedController('auth') export class AuthController { constructor(private readonly authService: AuthService) {} - @UseGuards(AuthGuard('local')) + @LocalGuard() @Post('/login') - @Docs('login') async login(@injectLocalUser() user: number): Promise { - return await this.authService.getToken(user); + return this.authService.getToken(user); } @Post('/signup') - @Docs('signup') async signup(@Body() body: SignupRequestDto): Promise { const id = await this.authService.signup(body); - return await this.authService.getToken(id); + return this.authService.getToken(id); } - @UseGuards(AuthGuard('jwt-refresh')) + @JwtRefreshGuard() @Post('/refresh') - @Docs('refresh') async refresh( @InjectRefreshUser() user: RefreshTokenPayload, ): Promise { - return await this.authService.refreshJWT(user); + return this.authService.refreshJWT(user); } - @UseGuards(AuthGuard('jwt-refresh')) + @JwtRefreshGuard() @Delete('/logout') - @Docs('logout') async logout(@InjectRefreshUser() user: RefreshTokenPayload): Promise { - await this.authService.logout(user); + return this.authService.logout(user); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Delete('/user-info') - @Docs('deleteUser') async deleteUser(@InjectAccessUser() user: JwtPayload): Promise { - await this.authService.deleteUser(user.id); + return this.authService.deleteUser(user.id); } @Post('/change-password/request') - @Docs('changePwdRequest') async changePwdRequest( @Body() body: ChangePasswordRequestDto, ): Promise { - await this.authService.changePwdRequest(body); + return this.authService.changePwdRequest(body); } @Post('/change-password/verify') - @Docs('verifyChangePwdCode') async verifyChangePwdCode( @Body() body: VerifyChangePasswordRequestDto, ): Promise { - await this.authService.verifyChangePwdCode(body); + return this.authService.verifyChangePwdCode(body); } @Put('/change-password') - @Docs('changePassword') async changePassword(@Body() body: ChangePasswordDto): Promise { - await this.authService.changePassword(body); + return this.authService.changePassword(body); } } diff --git a/src/domain/auth/auth.module.ts b/src/domain/auth/auth.module.ts index 2ec8d2c..8a00ce2 100644 --- a/src/domain/auth/auth.module.ts +++ b/src/domain/auth/auth.module.ts @@ -1,19 +1,19 @@ +import { ChannelModule } from '@/domain/channel/channel.module'; +import { MailerModule } from '@nestjs-modules/mailer'; import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; import { - KudogUser, ChangePwdAuthenticationEntity, EmailAuthenticationEntity, + KudogUser, RefreshTokenEntity, } from 'src/entities'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { JwtModule } from '@nestjs/jwt'; -import { JwtStrategy as RefreshStrategy } from './passport/refreshToken.strategy'; import { JwtStrategy as AccessStrategy } from './passport/accessToken.strategy'; import { LocalStrategy } from './passport/local.strategy'; -import { MailerModule } from '@nestjs-modules/mailer'; -import { ChannelModule } from 'src/channel/channel.module'; +import { JwtStrategy as RefreshStrategy } from './passport/refreshToken.strategy'; @Module({ imports: [ diff --git a/src/domain/auth/auth.service.ts b/src/domain/auth/auth.service.ts index 3c7e55f..09022a0 100644 --- a/src/domain/auth/auth.service.ts +++ b/src/domain/auth/auth.service.ts @@ -1,3 +1,6 @@ +import { JwtPayload, RefreshTokenPayload } from '@/common/types/auth'; +import { ChannelService } from '@/domain/channel/channel.service'; +import { MailerService } from '@nestjs-modules/mailer'; import { BadRequestException, HttpException, @@ -7,26 +10,23 @@ import { RequestTimeoutException, UnauthorizedException, } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { compare, hash } from 'bcrypt'; import { - KudogUser, ChangePwdAuthenticationEntity, EmailAuthenticationEntity, + KudogUser, RefreshTokenEntity, } from 'src/entities'; import { Repository } from 'typeorm'; -import { SignupRequestDto } from './dtos/signupRequest.dto'; -import { JwtService } from '@nestjs/jwt'; -import { JwtPayload, RefreshTokenPayload } from 'src/interfaces/auth'; -import { TokenResponseDto } from './dtos/tokenResponse.dto'; -import { MailerService } from '@nestjs-modules/mailer'; -import { ChannelService } from 'src/channel/channel.service'; import { ChangePasswordDto, ChangePasswordRequestDto, VerifyChangePasswordRequestDto, } from './dtos/changePwdRequest.dto'; +import { SignupRequestDto } from './dtos/signupRequest.dto'; +import { TokenResponseDto } from './dtos/tokenResponse.dto'; @Injectable() export class AuthService { constructor( diff --git a/src/entities/changePwd.entity.ts b/src/domain/auth/entities/changePwd.entity.ts similarity index 89% rename from src/entities/changePwd.entity.ts rename to src/domain/auth/entities/changePwd.entity.ts index 56d9364..88b6625 100644 --- a/src/entities/changePwd.entity.ts +++ b/src/domain/auth/entities/changePwd.entity.ts @@ -6,7 +6,7 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; -import { KudogUser } from './kudogUser.entity'; +import { KudogUser } from '../../../entities/kudogUser.entity'; @Entity('change_pwd_authentication') export class ChangePwdAuthenticationEntity { diff --git a/src/entities/emailAuthentication.entity.ts b/src/domain/auth/entities/emailAuthentication.entity.ts similarity index 100% rename from src/entities/emailAuthentication.entity.ts rename to src/domain/auth/entities/emailAuthentication.entity.ts diff --git a/src/entities/refreshToken.entity.ts b/src/domain/auth/entities/refreshToken.entity.ts similarity index 100% rename from src/entities/refreshToken.entity.ts rename to src/domain/auth/entities/refreshToken.entity.ts diff --git a/src/domain/auth/passport/accessToken.strategy.ts b/src/domain/auth/passport/accessToken.strategy.ts index 12e2bb4..50dcb4b 100644 --- a/src/domain/auth/passport/accessToken.strategy.ts +++ b/src/domain/auth/passport/accessToken.strategy.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { Strategy, ExtractJwt } from 'passport-jwt'; -import { JwtPayload } from 'src/interfaces/auth'; +import { JwtPayload } from '@/common/types/auth'; +import { Injectable, UseGuards, applyDecorators } from '@nestjs/common'; +import { AuthGuard, PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-access') { @@ -17,3 +17,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-access') { return payload; } } + +export function JwtAccessGuard() { + return UseGuards(AuthGuard('jwt-access')); +} diff --git a/src/domain/auth/passport/local.strategy.ts b/src/domain/auth/passport/local.strategy.ts index 2e5c3f9..f58ebae 100644 --- a/src/domain/auth/passport/local.strategy.ts +++ b/src/domain/auth/passport/local.strategy.ts @@ -1,6 +1,6 @@ +import { Injectable, UseGuards } from '@nestjs/common'; +import { AuthGuard, PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; -import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; import { AuthService } from '../auth.service'; @Injectable() @@ -13,3 +13,7 @@ export class LocalStrategy extends PassportStrategy(Strategy, 'local') { return await this.authService.validateUser(email, password); } } + +export function LocalGuard() { + return UseGuards(AuthGuard('local')); +} diff --git a/src/domain/auth/passport/refreshToken.strategy.ts b/src/domain/auth/passport/refreshToken.strategy.ts index befad8a..4553de6 100644 --- a/src/domain/auth/passport/refreshToken.strategy.ts +++ b/src/domain/auth/passport/refreshToken.strategy.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; +import { JwtPayload, RefreshTokenPayload } from '@/common/types/auth'; +import { Injectable, UseGuards } from '@nestjs/common'; +import { AuthGuard, PassportStrategy } from '@nestjs/passport'; import { Request } from 'express'; -import { Strategy, ExtractJwt } from 'passport-jwt'; -import { JwtPayload, RefreshTokenPayload } from 'src/interfaces/auth'; +import { ExtractJwt, Strategy } from 'passport-jwt'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { @@ -27,3 +27,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { }; } } + +export function JwtRefreshGuard() { + return UseGuards(AuthGuard('jwt-refresh')); +} diff --git a/src/domain/category/category.controller.ts b/src/domain/category/category.controller.ts index 9ee4d37..11da3d6 100644 --- a/src/domain/category/category.controller.ts +++ b/src/domain/category/category.controller.ts @@ -1,27 +1,25 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { CategoryService } from './category.service'; -import { AuthGuard } from '@nestjs/passport'; +import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { CategoryDocs } from '@/common/decorators/docs/category.decorator'; +import { JwtPayload } from '@/common/types/auth'; +import { Get, Param } from '@nestjs/common'; import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -import { InjectAccessUser } from 'src/decorators'; -import { JwtPayload } from 'src/interfaces/auth'; +import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { CategoryService } from './category.service'; import { ProviderListResponseDto } from './dtos/ProviderListResponse.dto'; import { CategoryListResponseDto } from './dtos/categoryListResponse.dto'; -import { Docs } from 'src/decorators/docs/category.decorator'; -@Controller('category') -@ApiTags('category') + +@CategoryDocs +@NamedController('category') export class CategoryController { constructor(private readonly categoryService: CategoryService) {} - @UseGuards(AuthGuard('jwt-access')) - @Docs('getProviders') + @JwtAccessGuard() @Get('/providers') async getProviders(): Promise { return this.categoryService.getProviders(); } - @UseGuards(AuthGuard('jwt-access')) - @Docs('getCategories') + @JwtAccessGuard() @Get('/by-providers/:id') async getCategories( @Param('id', IntValidationPipe) id: number, @@ -29,8 +27,7 @@ export class CategoryController { return this.categoryService.getCategories(id); } - @UseGuards(AuthGuard('jwt-access')) - @Docs('getBookmarkedProviders') + @JwtAccessGuard() @Get('/providers/bookmarks') async getBookmarkedProviders( @InjectAccessUser() user: JwtPayload, diff --git a/src/channel/channel.module.ts b/src/domain/channel/channel.module.ts similarity index 100% rename from src/channel/channel.module.ts rename to src/domain/channel/channel.module.ts diff --git a/src/channel/channel.service.ts b/src/domain/channel/channel.service.ts similarity index 70% rename from src/channel/channel.service.ts rename to src/domain/channel/channel.service.ts index aaa6c1a..9569aab 100644 --- a/src/channel/channel.service.ts +++ b/src/domain/channel/channel.service.ts @@ -20,11 +20,9 @@ export class ChannelService { await this.client.post(this.allChannelPath, { content: content.slice(0, sliceIndex), }); - await this.sendMessageToAll(content.slice(sliceIndex + 1)); - - return; + return this.sendMessageToAll(content.slice(sliceIndex + 1)); } - await this.client.post(this.allChannelPath, { content }); + this.client.post(this.allChannelPath, { content }); } async sendMessageToKudog(content: string) { @@ -34,37 +32,35 @@ export class ChannelService { await this.client.post(this.kudogChannelPath, { content: content.slice(0, sliceIndex), }); - await this.sendMessageToKudog(content.slice(sliceIndex + 1)); - - return; + return this.sendMessageToKudog(content.slice(sliceIndex + 1)); } - await this.client.post(this.kudogChannelPath, { content }); + this.client.post(this.kudogChannelPath, { content }); } createMessageFromNotices(noticeInfos: NotiByCategory[]): string { const messages = []; - noticeInfos.forEach((info) => { + for (const info of noticeInfos) { + if (info.notices.length === 0) continue; messages.push(`${info.category}에 새로운 공지사항이 올라왔습니다.`); - info.notices.forEach((notice) => { + for (const notice of info.notices) { const url = notice.url.endsWith('=') ? notice.url.slice(0, notice.url.lastIndexOf('&')) : notice.url; messages.push(`- [${notice.title}](${url})`); - }); - }); + } + } return messages.join('\n'); } createMessageFromNoticesWithOutURL(noticeInfos: NotiByCategory[]): string { const messages = []; - noticeInfos - .filter((info) => info.notices.length > 0) - .forEach((info) => { - messages.push(`${info.category}에 새로운 공지사항이 올라왔습니다.`); - info.notices.forEach((notice) => { - messages.push(`- ${notice.title}`); - }); - }); + for (const info of noticeInfos) { + if (info.notices.length === 0) continue; + messages.push(`${info.category}에 새로운 공지사항이 올라왔습니다.`); + for (const notice of info.notices) { + messages.push(`- [${notice.title}]`); + } + } messages.push('KUPID 공지사항은 앱에서 확인해주세요!'); return messages.join('\n'); } diff --git a/src/channel/dtos/notification.dto.ts b/src/domain/channel/dtos/notification.dto.ts similarity index 100% rename from src/channel/dtos/notification.dto.ts rename to src/domain/channel/dtos/notification.dto.ts diff --git a/src/domain/mail/mail.controller.ts b/src/domain/mail/mail.controller.ts index f4ed834..9b8c250 100644 --- a/src/domain/mail/mail.controller.ts +++ b/src/domain/mail/mail.controller.ts @@ -1,24 +1,22 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { MailService } from './mail.service'; -import { ApiTags } from '@nestjs/swagger'; -import { verifyRequestDto } from './dtos/verifyRequest.dto'; +import { NamedController } from '@/common/decorators'; +import { MailDocs } from '@/common/decorators/docs/mail.decorator'; +import { Body, Post } from '@nestjs/common'; import { verifyCodeRequestDto } from './dtos/verifyCodeRequest.dto'; -import { Docs } from 'src/decorators/docs/mail.decorator'; +import { verifyRequestDto } from './dtos/verifyRequest.dto'; +import { MailService } from './mail.service'; -@Controller('mail') -@ApiTags('mail') +@MailDocs +@NamedController('mail') export class MailController { constructor(private readonly mailService: MailService) {} @Post('/verify/send') - @Docs('sendVerifyMail') async sendVerifyMail(@Body() body: verifyRequestDto): Promise { - return await this.mailService.sendVerificationCode(body.email); + return this.mailService.sendVerificationCode(body.email); } @Post('/verify/check') - @Docs('checkVerifyCode') async checkVerifyCode(@Body() body: verifyCodeRequestDto): Promise { - return await this.mailService.checkVerificationCode(body.email, body.code); + return this.mailService.checkVerificationCode(body.email, body.code); } } diff --git a/src/domain/mail/mail.service.ts b/src/domain/mail/mail.service.ts index 08981d7..cc7be5e 100644 --- a/src/domain/mail/mail.service.ts +++ b/src/domain/mail/mail.service.ts @@ -15,7 +15,7 @@ import { import { Between, In, Repository } from 'typeorm'; import { MailerService } from '@nestjs-modules/mailer'; import { Cron, CronExpression } from '@nestjs/schedule'; -import { getHHMMdate, yesterdayTimeStamp } from 'src/utils/date'; +import { getHHMMdate, yesterdayTimeStamp } from '@/common/utils/date'; @Injectable() export class MailService { diff --git a/src/domain/notice/dtos/NoticeInfoResponse.dto.ts b/src/domain/notice/dtos/NoticeInfoResponse.dto.ts index a666e3a..960e314 100644 --- a/src/domain/notice/dtos/NoticeInfoResponse.dto.ts +++ b/src/domain/notice/dtos/NoticeInfoResponse.dto.ts @@ -119,12 +119,6 @@ export class NoticeInfoResponseDto { }) scrapBoxId: number[]; - @ApiProperty({ - description: '전처리된 카테고리', - example: '장학정보', - }) - mappedCategory: string; - constructor(entity: Notice, scrapBoxes: ScrapBoxEntity[] = []) { const scrapBoxIds = scrapBoxes .filter((scrapBox) => @@ -143,7 +137,6 @@ export class NoticeInfoResponseDto { this.scrapCount = entity.scraps.length; this.provider = entity.category.provider.name; this.category = entity.category.name; - this.mappedCategory = entity.category.mappedCategory; this.scrapBoxId = scrapBoxIds; } } diff --git a/src/domain/notice/notice.controller.ts b/src/domain/notice/notice.controller.ts index ec59dbe..2d88d00 100644 --- a/src/domain/notice/notice.controller.ts +++ b/src/domain/notice/notice.controller.ts @@ -1,43 +1,32 @@ -import { - Body, - Controller, - Get, - Param, - Post, - Put, - Query, - UseGuards, -} from '@nestjs/common'; -import { NoticeService } from './notice.service'; -import { ApiTags } from '@nestjs/swagger'; -import { NoticeListResponseDto } from './dtos/NoticeListResponse.dto'; -import { AuthGuard } from '@nestjs/passport'; +import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { UsePagination } from '@/common/decorators'; +import { NoticeDocs } from '@/common/decorators/docs'; +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; +import { JwtPayload } from '@/common/types/auth'; +import { Body, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; +import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { AddRequestRequestDto } from './dtos/AddRequestRequest.dto'; import { NoticeFilterRequestDto } from './dtos/NoticeFilterRequest.dto'; -import { Docs } from 'src/decorators/docs/notice.decorator'; -import { InjectAccessUser } from 'src/decorators'; -import { JwtPayload } from 'src/interfaces/auth'; import { NoticeInfoResponseDto } from './dtos/NoticeInfoResponse.dto'; -import { UsePagination } from 'src/decorators'; -import { PageQuery } from 'src/interfaces/pageQuery'; -import { PageResponse } from 'src/interfaces/pageResponse'; -import { AddRequestRequestDto } from './dtos/AddRequestRequest.dto'; -import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; +import { NoticeListResponseDto } from './dtos/NoticeListResponse.dto'; +import { NoticeService } from './notice.service'; -@Controller('notice') -@ApiTags('notice') +@NoticeDocs +@NamedController('notice') export class NoticeController { constructor(private readonly noticeService: NoticeService) {} - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/list') - @Docs('getNoticeList') async getNoticeList( @InjectAccessUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, @Query() filter: NoticeFilterRequestDto, @Query('keyword') keyword?: string, ): Promise> { - return await this.noticeService.getNoticeList( + return this.noticeService.getNoticeList( user.id, pageQuery, filter, @@ -45,34 +34,31 @@ export class NoticeController { ); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Put('/:noticeId/scrap/:scrapBoxId') - @Docs('scrapNotice') async scrapNotice( @InjectAccessUser() user: JwtPayload, @Param('noticeId', IntValidationPipe) noticeId: number, @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, ): Promise { - return await this.noticeService.scrapNotice(user.id, noticeId, scrapBoxId); + return this.noticeService.scrapNotice(user.id, noticeId, scrapBoxId); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/info/:id') - @Docs('getNoticeInfoById') async getNoticeInfoById( @Param('id', IntValidationPipe) id: number, @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.noticeService.getNoticeInfoById(id, user.id); + return this.noticeService.getNoticeInfoById(id, user.id); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Post('/add-request') - @Docs('addNoticeRequest') async addNoticeRequest( @InjectAccessUser() user: JwtPayload, @Body() body: AddRequestRequestDto, ): Promise { - return await this.noticeService.addNoticeRequest(body); + return this.noticeService.addNoticeRequest(body); } } diff --git a/src/domain/notice/notice.module.ts b/src/domain/notice/notice.module.ts index 5020f2c..72b6e09 100644 --- a/src/domain/notice/notice.module.ts +++ b/src/domain/notice/notice.module.ts @@ -1,9 +1,9 @@ +import { ChannelService } from '@/domain/channel/channel.service'; import { Module } from '@nestjs/common'; -import { NoticeService } from './notice.service'; -import { NoticeController } from './notice.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Notice, ScrapEntity, ScrapBoxEntity } from 'src/entities'; -import { ChannelService } from 'src/channel/channel.service'; +import { Notice, ScrapBoxEntity, ScrapEntity } from 'src/entities'; +import { NoticeController } from './notice.controller'; +import { NoticeService } from './notice.service'; @Module({ providers: [NoticeService, ChannelService], diff --git a/src/domain/notice/notice.service.ts b/src/domain/notice/notice.service.ts index e4bd910..e368c1e 100644 --- a/src/domain/notice/notice.service.ts +++ b/src/domain/notice/notice.service.ts @@ -1,18 +1,18 @@ +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; +import { ChannelService } from '@/domain/channel/channel.service'; import { ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Notice, ScrapEntity, ScrapBoxEntity } from 'src/entities'; +import { Notice, ScrapBoxEntity, ScrapEntity } from 'src/entities'; import { Repository } from 'typeorm'; -import { NoticeListResponseDto } from './dtos/NoticeListResponse.dto'; -import { NoticeInfoResponseDto } from './dtos/NoticeInfoResponse.dto'; -import { NoticeFilterRequestDto } from './dtos/NoticeFilterRequest.dto'; -import { PageResponse } from 'src/interfaces/pageResponse'; -import { PageQuery } from 'src/interfaces/pageQuery'; -import { ChannelService } from 'src/channel/channel.service'; import { AddRequestRequestDto } from './dtos/AddRequestRequest.dto'; +import { NoticeFilterRequestDto } from './dtos/NoticeFilterRequest.dto'; +import { NoticeInfoResponseDto } from './dtos/NoticeInfoResponse.dto'; +import { NoticeListResponseDto } from './dtos/NoticeListResponse.dto'; @Injectable() export class NoticeService { diff --git a/src/domain/notification/notification.controller.ts b/src/domain/notification/notification.controller.ts index 3ed320b..6e4a174 100644 --- a/src/domain/notification/notification.controller.ts +++ b/src/domain/notification/notification.controller.ts @@ -1,83 +1,72 @@ -import { Controller, Get, Body, UseGuards, Post, Delete } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; -import { AuthGuard } from '@nestjs/passport'; -import { InjectAccessUser } from 'src/decorators'; -import { JwtPayload } from 'src/interfaces/auth'; -import { Docs } from 'src/decorators/docs/notification.decorator'; -import { NotificationService } from './notification.service'; +import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { UsePagination } from '@/common/decorators'; +import { NotificationDocs } from '@/common/decorators/docs'; +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; +import { JwtPayload } from '@/common/types/auth'; +import { Body, Delete, Get, Post } from '@nestjs/common'; +import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; import { NotificationInfoResponseDto } from './dtos/noticiationInfoResponse.dto'; -import { PageResponse } from 'src/interfaces/pageResponse'; import { TokenRequestDto } from './dtos/tokenRequest.dto'; -import { PageQuery } from 'src/interfaces/pageQuery'; -import { UsePagination } from 'src/decorators'; +import { NotificationService } from './notification.service'; -@Controller('notifications') -@ApiTags('notifications') +@NotificationDocs +@NamedController('notification') export class NotificationController { constructor(private readonly notificationService: NotificationService) {} - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('') - @Docs('getNotifications') async getNotifications( @InjectAccessUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { - return await this.notificationService.getNotifications(user.id, pageQuery); + return this.notificationService.getNotifications(user.id, pageQuery); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/new') - @Docs('getNewNotifications') async getNewNotifications( @InjectAccessUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { - return await this.notificationService.getNewNotifications( + return this.notificationService.getNewNotifications( user.id, pageQuery, ); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Post('/token') - @Docs('registerToken') async registerToken( @InjectAccessUser() user: JwtPayload, @Body() body: TokenRequestDto, ): Promise { - return await this.notificationService.registerToken(user.id, body.token); + return this.notificationService.registerToken(user.id, body.token); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Delete('/token') - @Docs('deleteToken') async deleteToken( @InjectAccessUser() user: JwtPayload, @Body() body: TokenRequestDto, ): Promise { - return await this.notificationService.deleteToken(user.id, body.token); + return this.notificationService.deleteToken(user.id, body.token); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/status') - @Docs('getTokenStatus') async getTokenStatus( @InjectAccessUser() user: JwtPayload, @Body() body: TokenRequestDto, ): Promise { - return await this.notificationService.getTokenStatus(user.id, body.token); + return this.notificationService.getTokenStatus(user.id, body.token); } - @UseGuards(AuthGuard('jwt-access')) - @ApiOperation({ - summary: 'FCM test', - description: - 'JWT만 보내주면, 해당 유저가 등록한 기기에 모두 알림을 보냅니다', - }) + @JwtAccessGuard() @Get('/test') async sendNotification(@InjectAccessUser() user: JwtPayload): Promise { - return await this.notificationService.sendNotification( + return this.notificationService.sendNotification( [user.id], 'test', 'test', diff --git a/src/domain/notification/notification.module.ts b/src/domain/notification/notification.module.ts index d77fc49..4c44249 100644 --- a/src/domain/notification/notification.module.ts +++ b/src/domain/notification/notification.module.ts @@ -1,14 +1,14 @@ +import { ChannelService } from '@/domain/channel/channel.service'; import { Module } from '@nestjs/common'; -import { NotificationService } from './notification.service'; -import { NotificationController } from './notification.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CategoryPerSubscribeBoxEntity, - NotificationTokenEntity, - NotificationEntity, Notice, + NotificationEntity, + NotificationTokenEntity, } from 'src/entities'; -import { ChannelService } from 'src/channel/channel.service'; +import { NotificationController } from './notification.controller'; +import { NotificationService } from './notification.service'; @Module({ imports: [ diff --git a/src/domain/notification/notification.service.ts b/src/domain/notification/notification.service.ts index 538bfa2..9b4a1bf 100644 --- a/src/domain/notification/notification.service.ts +++ b/src/domain/notification/notification.service.ts @@ -1,18 +1,18 @@ +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; +import { ChannelService } from '@/domain/channel/channel.service'; +import { NotiByCategory } from '@/domain/channel/dtos/notification.dto'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; import * as firebase from 'firebase-admin'; -import { NotificationInfoResponseDto } from './dtos/noticiationInfoResponse.dto'; -import { PageResponse } from 'src/interfaces/pageResponse'; import { CategoryPerSubscribeBoxEntity, - NotificationTokenEntity, - NotificationEntity, Notice, + NotificationEntity, + NotificationTokenEntity, } from 'src/entities'; -import { PageQuery } from 'src/interfaces/pageQuery'; -import { NotiByCategory } from 'src/channel/dtos/notification.dto'; -import { ChannelService } from 'src/channel/channel.service'; +import { In, Repository } from 'typeorm'; +import { NotificationInfoResponseDto } from './dtos/noticiationInfoResponse.dto'; @Injectable() export class NotificationService { diff --git a/src/domain/scrap/scrap.controller.ts b/src/domain/scrap/scrap.controller.ts index 6098d5f..e88caf6 100644 --- a/src/domain/scrap/scrap.controller.ts +++ b/src/domain/scrap/scrap.controller.ts @@ -1,79 +1,77 @@ +import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { UsePagination } from '@/common/decorators'; + +import { ScrapDocs } from '@/common/decorators/docs'; +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; +import { JwtPayload } from '@/common/types/auth'; import { Body, Controller, + Delete, Get, - Post, Param, + Post, Put, - Delete, UseGuards, } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; import { AuthGuard } from '@nestjs/passport'; -import { ScrapService } from './scrap.service'; +import { ApiTags } from '@nestjs/swagger'; +import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; +import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; import { ScrapBoxRequestDto } from './dtos/scrapBoxRequest.dto'; -import { InjectAccessUser } from 'src/decorators'; -import { JwtPayload } from 'src/interfaces/auth'; import { ScrapBoxResponseDto } from './dtos/scrapBoxResponse.dto'; -import { Docs } from 'src/decorators/docs/scrap.decorator'; import { ScrapBoxResponseWithNotices } from './dtos/scrapBoxResponseWithNotices.dto'; -import { UsePagination } from 'src/decorators'; -import { PageQuery } from 'src/interfaces/pageQuery'; -import { PageResponse } from 'src/interfaces/pageResponse'; -import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -@ApiTags('Scrap') -@Controller('scrap') +import { ScrapService } from './scrap.service'; + +@ScrapDocs +@NamedController('scrap') export class ScrapController { constructor(private readonly scrapService: ScrapService) {} - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Post('/box') - @Docs('createScrapBox') async createScrapBox( @Body() body: ScrapBoxRequestDto, @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.scrapService.createScrapBox(user.id, body); + return this.scrapService.createScrapBox(user.id, body); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/box/:scrapBoxId') - @Docs('getScrapBoxInfo') async getScrapBoxInfo( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.scrapService.getScrapBoxInfo(user.id, scrapBoxId); + return this.scrapService.getScrapBoxInfo(user.id, scrapBoxId); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/box') - @Docs('getScrapBoxes') async getScrapBoxes( @InjectAccessUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { - return await this.scrapService.getScrapBoxes(user.id, pageQuery); + return this.scrapService.getScrapBoxes(user.id, pageQuery); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Put('/box/:scrapBoxId') - @Docs('updateScrapBox') async updateScrapBox( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, @InjectAccessUser() user: JwtPayload, @Body() body: ScrapBoxRequestDto, ): Promise { - return await this.scrapService.updateScrapBox(scrapBoxId, user.id, body); + return this.scrapService.updateScrapBox(scrapBoxId, user.id, body); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Delete('/box/:scrapBoxId') - @Docs('deleteScrapBox') async deleteScrapBox( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.scrapService.deleteScrapBox(scrapBoxId, user.id); + return this.scrapService.deleteScrapBox(scrapBoxId, user.id); } } diff --git a/src/domain/scrap/scrap.service.ts b/src/domain/scrap/scrap.service.ts index 7f92358..7538739 100644 --- a/src/domain/scrap/scrap.service.ts +++ b/src/domain/scrap/scrap.service.ts @@ -1,3 +1,5 @@ +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; import { ForbiddenException, Injectable, @@ -7,11 +9,9 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { ScrapBoxEntity } from 'src/entities'; import { Repository } from 'typeorm'; -import { ScrapBoxResponseDto } from './dtos/scrapBoxResponse.dto'; import { ScrapBoxRequestDto } from './dtos/scrapBoxRequest.dto'; +import { ScrapBoxResponseDto } from './dtos/scrapBoxResponse.dto'; import { ScrapBoxResponseWithNotices } from './dtos/scrapBoxResponseWithNotices.dto'; -import { PageResponse } from 'src/interfaces/pageResponse'; -import { PageQuery } from 'src/interfaces/pageQuery'; @Injectable() export class ScrapService { diff --git a/src/domain/subscribe/subscribe.controller.ts b/src/domain/subscribe/subscribe.controller.ts index e6cb103..082822c 100644 --- a/src/domain/subscribe/subscribe.controller.ts +++ b/src/domain/subscribe/subscribe.controller.ts @@ -1,106 +1,94 @@ +import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { UsePagination } from '@/common/decorators'; +import { SubscribeDocs } from '@/common/decorators/docs'; +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; +import { JwtPayload } from '@/common/types/auth'; import { Body, - Controller, Delete, Get, Param, Post, Put, Query, - UseGuards, } from '@nestjs/common'; -import { SubscribeService } from './subscribe.service'; -import { AuthGuard } from '@nestjs/passport'; -import { Docs } from 'src/decorators/docs/subscribe.decorator'; -import { SubscribeBoxRequestDto } from './dtos/subscribeBoxRequest.dto'; -import { JwtPayload } from 'src/interfaces/auth'; -import { InjectAccessUser } from 'src/decorators'; -import { SubscribeBoxResponseDtoWithNotices } from './dtos/subscribeBoxResponseWithNotices.dto'; -import { SubscribeBoxResponseDto } from './dtos/subscribeBoxResponse.dto'; -import { ApiTags } from '@nestjs/swagger'; -import { PageResponse } from 'src/interfaces/pageResponse'; -import { UsePagination } from 'src/decorators'; -import { PageQuery } from 'src/interfaces/pageQuery'; import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; +import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; import { NoticeListResponseDto } from '../notice/dtos/NoticeListResponse.dto'; +import { SubscribeBoxRequestDto } from './dtos/subscribeBoxRequest.dto'; +import { SubscribeBoxResponseDto } from './dtos/subscribeBoxResponse.dto'; +import { SubscribeBoxResponseDtoWithNotices } from './dtos/subscribeBoxResponseWithNotices.dto'; +import { SubscribeService } from './subscribe.service'; -@ApiTags('Subscribe') -@Controller('subscribe') +@SubscribeDocs +@NamedController('subscribe') export class SubscribeController { constructor(private readonly subscribeService: SubscribeService) {} - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Post('/box') - @Docs('createSubscribeBox') async createSubscribeBox( @Body() body: SubscribeBoxRequestDto, @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.subscribeService.createSubscribeBox(user.id, body); + return this.subscribeService.createSubscribeBox(user.id, body); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/box/:subscribeBoxId') - @Docs('getSubscribeBoxInfo') async getSubscribeInfo( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, @InjectAccessUser() user: JwtPayload, @Query('date') date: string, ): Promise { - return await this.subscribeService.getSubscribeBoxInfo( + return this.subscribeService.getSubscribeBoxInfo( user.id, subscribeBoxId, date, ); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/box') - @Docs('getSubscribeBoxes') - async getSubscribees( + async getSubscribeBoxes( @InjectAccessUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { - return await this.subscribeService.getSubscribeBoxes(user.id, pageQuery); + return this.subscribeService.getSubscribeBoxes(user.id, pageQuery); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Put('/box/:subscribeBoxId') - @Docs('updateSubscribeBox') - async updateSubscribe( + async updateSubscribeBox( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, @InjectAccessUser() user: JwtPayload, @Body() body: SubscribeBoxRequestDto, ): Promise { - return await this.subscribeService.updateSubscribeBox( + return this.subscribeService.updateSubscribeBox( subscribeBoxId, user.id, body, ); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Delete('/box/:subscribeBoxId') - @Docs('deleteSubscribeBox') - async deleteSubscribe( + async deleteSubscribeBox( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.subscribeService.deleteSubscribeBox( - subscribeBoxId, - user.id, - ); + return this.subscribeService.deleteSubscribeBox(subscribeBoxId, user.id); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/box/:subscribeBoxId/notices') - @Docs('getNoticesByBoxWithDate') async getNoticesByBoxWithDate( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, @InjectAccessUser() user: JwtPayload, @Query('date') date: string, ): Promise { - return await this.subscribeService.getNoticesByBoxWithDate( + return this.subscribeService.getNoticesByBoxWithDate( date, subscribeBoxId, user.id, diff --git a/src/domain/subscribe/subscribe.service.ts b/src/domain/subscribe/subscribe.service.ts index c7fa1b7..ef00486 100644 --- a/src/domain/subscribe/subscribe.service.ts +++ b/src/domain/subscribe/subscribe.service.ts @@ -1,12 +1,11 @@ +import { PageQuery } from '@/common/dtos/pageQuery'; +import { PageResponse } from '@/common/dtos/pageResponse'; import { ForbiddenException, Injectable, NotFoundException, } from '@nestjs/common'; -import { SubscribeBoxRequestDto } from './dtos/subscribeBoxRequest.dto'; -import { SubscribeBoxResponseDto } from './dtos/subscribeBoxResponse.dto'; import { InjectRepository } from '@nestjs/typeorm'; -import { Between, In, Repository } from 'typeorm'; import { CategoryEntity, CategoryPerSubscribeBoxEntity, @@ -14,10 +13,11 @@ import { ScrapBoxEntity, SubscribeBoxEntity, } from 'src/entities'; -import { SubscribeBoxResponseDtoWithNotices } from './dtos/subscribeBoxResponseWithNotices.dto'; -import { PageResponse } from 'src/interfaces/pageResponse'; -import { PageQuery } from 'src/interfaces/pageQuery'; +import { Between, In, Repository } from 'typeorm'; import { NoticeListResponseDto } from '../notice/dtos/NoticeListResponse.dto'; +import { SubscribeBoxRequestDto } from './dtos/subscribeBoxRequest.dto'; +import { SubscribeBoxResponseDto } from './dtos/subscribeBoxResponse.dto'; +import { SubscribeBoxResponseDtoWithNotices } from './dtos/subscribeBoxResponseWithNotices.dto'; @Injectable() export class SubscribeService { @@ -221,7 +221,9 @@ export class SubscribeService { if (subscribeBox.user.id !== userId) throw new ForbiddenException('권한이 없습니다'); - const [hours, mins] = subscribeBox.user.sendTime.split(':').map(parseInt); + const [hours, mins] = subscribeBox.user.sendTime + .split(':') + .map(Number.parseInt); const sendTimeInMs = hours * 60 * 60 * 1000 + mins * 60 * 1000; const to = new Date(dateString).getTime() + sendTimeInMs; diff --git a/src/domain/users/users.controller.ts b/src/domain/users/users.controller.ts index 424c55b..340b43c 100644 --- a/src/domain/users/users.controller.ts +++ b/src/domain/users/users.controller.ts @@ -1,33 +1,30 @@ -import { Body, Controller, Get, Put, UseGuards } from '@nestjs/common'; -import { UsersService } from './users.service'; -import { AuthGuard } from '@nestjs/passport'; -import { ApiTags } from '@nestjs/swagger'; +import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { UserDocs } from '@/common/decorators/docs'; +import { JwtPayload } from '@/common/types/auth'; +import { Body, Get, Put } from '@nestjs/common'; +import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; import { ModifyInfoRequestDto, UserInfoResponseDto } from './dtos/userInfo.dto'; -import { Docs } from 'src/decorators/docs/user.decorator'; -import { InjectAccessUser } from 'src/decorators'; -import { JwtPayload } from 'src/interfaces/auth'; +import { UsersService } from './users.service'; -@Controller('users') -@ApiTags('user') +@UserDocs +@NamedController('users') export class UsersController { constructor(private readonly userService: UsersService) {} - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Get('/info') - @Docs('getUserInfo') async getUserInfo( @InjectAccessUser() user: JwtPayload, ): Promise { - return await this.userService.getUserInfo(user.id); + return this.userService.getUserInfo(user.id); } - @UseGuards(AuthGuard('jwt-access')) + @JwtAccessGuard() @Put('/info') - @Docs('modifyUserInfo') async modifyUserInfo( @InjectAccessUser() user: JwtPayload, @Body() body: ModifyInfoRequestDto, ): Promise { - return await this.userService.modifyUserInfo(user.id, body); + return this.userService.modifyUserInfo(user.id, body); } } diff --git a/src/entities/category.entity.ts b/src/entities/category.entity.ts index deafbce..b7cd88b 100644 --- a/src/entities/category.entity.ts +++ b/src/entities/category.entity.ts @@ -1,3 +1,5 @@ +import { Notice } from 'src/entities'; +import { ProviderEntity } from 'src/entities'; import { Column, Entity, @@ -5,9 +7,6 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { Notice } from 'src/entities'; -import { ProviderEntity } from 'src/entities'; -import { CategoryMap } from 'src/enums'; import { CategoryPerSubscribeBoxEntity } from './categoryPerSubscribes.entity'; @Entity('category') @@ -21,9 +20,6 @@ export class CategoryEntity { @Column() url: string; - @Column({ type: 'enum', enum: CategoryMap, default: CategoryMap.공지사항 }) - mappedCategory: CategoryMap; - @ManyToOne( () => ProviderEntity, (provider) => provider.categories, diff --git a/src/entities/index.ts b/src/entities/index.ts index f75fe72..4c84453 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,14 +1,14 @@ export * from './category.entity'; export * from './kudogUser.entity'; -export * from './emailAuthentication.entity'; +export * from '../domain/auth/entities/emailAuthentication.entity'; export * from './notice.entity'; export * from './provider.entity'; -export * from './changePwd.entity'; +export * from '../domain/auth/entities/changePwd.entity'; export * from './scrap.entity'; export * from './scrapBox.entity'; export * from './subscribeBox.entity'; export * from './categoryPerSubscribes.entity'; export * from './notificationToken.entity'; export * from './notification.entity'; -export * from './refreshToken.entity'; +export * from '../domain/auth/entities/refreshToken.entity'; export * from './providerBookmark.entity'; diff --git a/src/enums/categoryMap.enum.ts b/src/enums/categoryMap.enum.ts deleted file mode 100644 index 5c9000d..0000000 --- a/src/enums/categoryMap.enum.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum CategoryMap { - 공지사항 = '공지사항', - '행사 소식' = '행사 소식', - '진로 정보' = '진로 정보', - '공모전 소식' = '공모전 소식', - '채용 정보' = '채용 정보', - '대학원 공지사항' = '대학원 공지사항', - '장학 공지' = '장학 공지', - '대학원 장학 공지' = '대학원 장학 공지', - '학사 일정' = '학사 일정', - '대학원 학사 일정' = '대학원 학사 일정', -} diff --git a/src/enums/index.ts b/src/enums/index.ts deleted file mode 100644 index 1b0a3fc..0000000 --- a/src/enums/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './categoryMap.enum'; diff --git a/src/interfaces/dto.ts b/src/fetch/dto.ts similarity index 100% rename from src/interfaces/dto.ts rename to src/fetch/dto.ts diff --git a/src/fetch/fetch.module.ts b/src/fetch/fetch.module.ts index 18d9371..10b13b2 100644 --- a/src/fetch/fetch.module.ts +++ b/src/fetch/fetch.module.ts @@ -1,15 +1,15 @@ +import { ChannelService } from '@/domain/channel/channel.service'; import { Module } from '@nestjs/common'; -import { FetchService } from './fetch.service'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { MailModule } from 'src/domain/mail/mail.module'; +import { NotificationModule } from 'src/domain/notification/notification.module'; import { CategoryEntity, KudogUser, Notice, ProviderEntity, } from 'src/entities'; -import { ChannelService } from 'src/channel/channel.service'; -import { MailModule } from 'src/domain/mail/mail.module'; -import { NotificationModule } from 'src/domain/notification/notification.module'; +import { FetchService } from './fetch.service'; @Module({ providers: [FetchService, ChannelService], diff --git a/src/fetch/fetch.service.ts b/src/fetch/fetch.service.ts index 7c9685b..d7510a7 100644 --- a/src/fetch/fetch.service.ts +++ b/src/fetch/fetch.service.ts @@ -1,3 +1,4 @@ +import { ChannelService } from '@/domain/channel/channel.service'; import { Injectable, InternalServerErrorException, @@ -5,11 +6,10 @@ import { } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; +import { NotificationService } from 'src/domain/notification/notification.service'; import { CategoryEntity, Notice } from 'src/entities'; import { Not, Repository } from 'typeorm'; import noticeFetcher from './fetch'; -import { ChannelService } from 'src/channel/channel.service'; -import { NotificationService } from 'src/domain/notification/notification.service'; @Injectable() export class FetchService { diff --git a/src/fetch/fetch.ts b/src/fetch/fetch.ts index 0cee54b..c307e9b 100644 --- a/src/fetch/fetch.ts +++ b/src/fetch/fetch.ts @@ -1,9 +1,9 @@ -import { page, url } from 'src/interfaces/urls'; //6시에 올라온 공지를 가져오는 코드. notice에 저장 -import { dto } from 'src/interfaces/dto'; +import { dto } from '@/fetch/dto'; +import { url, page } from '@/fetch/urls'; //6시에 올라온 공지를 가져오는 코드. notice에 저장 import { AnyNode } from 'domhandler'; -import { parseDocument, DomUtils } from 'htmlparser2'; import axios from 'axios'; +import { DomUtils, parseDocument } from 'htmlparser2'; const dateRegex = /\d{4}\.(0[1-9]|1[012])\.(0[1-9]|[12][0-9]|3[01])/; diff --git a/src/interfaces/urls.ts b/src/fetch/urls.ts similarity index 100% rename from src/interfaces/urls.ts rename to src/fetch/urls.ts diff --git a/src/filters/httpException.filter.ts b/src/filters/httpException.filter.ts index 6bbdb82..6c0bc98 100644 --- a/src/filters/httpException.filter.ts +++ b/src/filters/httpException.filter.ts @@ -1,26 +1,19 @@ -import { - ArgumentsHost, - Catch, - ExceptionFilter, - HttpException, -} from '@nestjs/common'; +import { HttpException } from '@/common/utils/exception'; +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; import { Response } from 'express'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { - constructor() {} catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); + const { statusCode, errorCode, message, name } = exception; - const status = exception.getStatus(); - const message = exception.message; - const name = exception.name; - - response.status(status).json({ - status, - message: message, - name: name, + response.status(statusCode).json({ + statusCode, + errorCode, + message, + name, }); } } diff --git a/src/filters/internalError.filter.ts b/src/filters/internalError.filter.ts index aa0dbd7..d1244df 100644 --- a/src/filters/internalError.filter.ts +++ b/src/filters/internalError.filter.ts @@ -1,38 +1,23 @@ +import { EXCEPTIONS } from '@/common/utils/exception'; +import { ChannelService } from '@/domain/channel/channel.service'; import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; import { Response } from 'express'; -import { ChannelService } from 'src/channel/channel.service'; @Catch() export class InternalErrorFilter implements ExceptionFilter { constructor(private readonly channelService: ChannelService) {} - catch(exception: any, host: ArgumentsHost) { + catch(exception: Error, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); - - this.channelService - .sendMessageToKudog('internal error report : ') - .then(async () => { - exception.name && - (await this.channelService.sendMessageToKudog( - 'name : ' + exception.name, - )); - }) - .then(async () => { - exception.message && - (await this.channelService.sendMessageToKudog( - 'message : ' + exception.message, - )); - }) - .then(async () => { - exception.stack && - (await this.channelService.sendMessageToKudog( - 'stack : ' + exception.stack, - )); - }); - + const notificationMessage = `internal error report : ${exception.name}\n${exception.message}\n${exception.stack}`; + this.channelService.sendMessageToKudog(notificationMessage); + const { statusCode, errorCode, name, message } = + EXCEPTIONS.INTERNAL_SERVER_ERROR; response.status(500).json({ - status: 500, - message: 'Internal Server Error', + statusCode, + errorCode, + message, + name, }); } } diff --git a/src/interfaces/docsException.ts b/src/interfaces/docsException.ts deleted file mode 100644 index 4132813..0000000 --- a/src/interfaces/docsException.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class DocumentedException { - @ApiProperty({ example: 401 }) - status: number; - - @ApiProperty({ example: '인증되지 않은 이메일입니다.' }) - message: string; - - @ApiProperty({ example: 'UnauthorizedException' }) - name: string; - - response: { - message: string; - error: string; - statusCode: number; - }; -} diff --git a/src/main.ts b/src/main.ts index ffb4131..3f0fdbb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import { ExceptionFilter, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; -import { ChannelService } from './channel/channel.service'; +import { ChannelService } from './domain/channel/channel.service'; import { HttpExceptionFilter } from './filters/httpException.filter'; import { InternalErrorFilter } from './filters/internalError.filter'; @@ -34,8 +34,8 @@ async function bootstrap() { filters.push(new InternalErrorFilter(channelService)); } filters.push(new HttpExceptionFilter()); - - app.useLogger(app.get(Logger)); + const logger = app.get(Logger); + app.useLogger(logger); app.useGlobalFilters(...filters); app.useGlobalPipes(new ValidationPipe({ transform: true })); From f4fafb0732ac3704c561e124b0924c71db932c7c Mon Sep 17 00:00:00 2001 From: overthestream Date: Sat, 27 Jul 2024 03:04:06 +0900 Subject: [PATCH 05/11] rename decorators --- ...=> apiKudogExceptionResponse.decorator.ts} | 0 src/common/decorators/docs/auth.decorator.ts | 6 ++--- .../decorators/docs/category.decorator.ts | 7 +++--- .../decorators/docs/common.decorator.ts | 3 +-- src/common/decorators/docs/mail.decorator.ts | 3 +-- .../decorators/docs/notice.decorator.ts | 6 ++--- .../decorators/docs/notification.decorator.ts | 4 ++-- src/common/decorators/docs/scrap.decorator.ts | 6 ++--- .../decorators/docs/subscribe.decorator.ts | 8 +++---- src/common/decorators/docs/user.decorator.ts | 2 +- src/common/decorators/index.ts | 5 ++-- .../decorators/injectAccessUser.decorator.ts | 9 -------- .../decorators/injectLocalUser.decorator.ts | 8 ------- .../decorators/injectRefreshUser.decorator.ts | 9 -------- src/common/decorators/injectUser.decorator.ts | 23 +++++++++++++++++++ 15 files changed, 46 insertions(+), 53 deletions(-) rename src/common/decorators/{apiKudogExceptionRespone.decorator.ts => apiKudogExceptionResponse.decorator.ts} (100%) delete mode 100644 src/common/decorators/injectAccessUser.decorator.ts delete mode 100644 src/common/decorators/injectLocalUser.decorator.ts delete mode 100644 src/common/decorators/injectRefreshUser.decorator.ts create mode 100644 src/common/decorators/injectUser.decorator.ts diff --git a/src/common/decorators/apiKudogExceptionRespone.decorator.ts b/src/common/decorators/apiKudogExceptionResponse.decorator.ts similarity index 100% rename from src/common/decorators/apiKudogExceptionRespone.decorator.ts rename to src/common/decorators/apiKudogExceptionResponse.decorator.ts diff --git a/src/common/decorators/docs/auth.decorator.ts b/src/common/decorators/docs/auth.decorator.ts index d8a6df8..36c0316 100644 --- a/src/common/decorators/docs/auth.decorator.ts +++ b/src/common/decorators/docs/auth.decorator.ts @@ -1,5 +1,7 @@ import type { MethodNames } from '@/common/types/method'; import type { AuthController } from '@/domain/auth/auth.controller'; +import { LoginRequestDto } from '@/domain/auth/dtos/loginRequestDto'; +import { TokenResponseDto } from '@/domain/auth/dtos/tokenResponse.dto'; import { ApiBody, ApiCreatedResponse, @@ -7,9 +9,7 @@ import { ApiOkResponse, ApiOperation, } from '@nestjs/swagger'; -import { LoginRequestDto } from 'src/domain/auth/dtos/loginRequestDto'; -import { TokenResponseDto } from 'src/domain/auth/dtos/tokenResponse.dto'; -import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; +import { ApiKudogExceptionResponse } from '@/common/decorators'; type AuthEndpoints = MethodNames; diff --git a/src/common/decorators/docs/category.decorator.ts b/src/common/decorators/docs/category.decorator.ts index faa407b..24afbc1 100644 --- a/src/common/decorators/docs/category.decorator.ts +++ b/src/common/decorators/docs/category.decorator.ts @@ -1,10 +1,9 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; import type { MethodNames } from '@/common/types/method'; import type { CategoryController } from '@/domain/category/category.controller'; +import { ProviderListResponseDto } from '@/domain/category/dtos/ProviderListResponse.dto'; +import { CategoryListResponseDto } from '@/domain/category/dtos/categoryListResponse.dto'; import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; -import { ProviderListResponseDto } from 'src/domain/category/dtos/ProviderListResponse.dto'; -import { CategoryListResponseDto } from 'src/domain/category/dtos/categoryListResponse.dto'; -import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; - type CategoryEndpoints = MethodNames; const CategoryDocsMap: Record = { diff --git a/src/common/decorators/docs/common.decorator.ts b/src/common/decorators/docs/common.decorator.ts index 0b69dbe..ad1c08f 100644 --- a/src/common/decorators/docs/common.decorator.ts +++ b/src/common/decorators/docs/common.decorator.ts @@ -1,8 +1,7 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; import { PageResponse } from '@/common/dtos/pageResponse'; import { applyDecorators } from '@nestjs/common'; import { ApiDefaultResponse, ApiQuery } from '@nestjs/swagger'; -import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; - export function ApiPagination() { return applyDecorators( ApiQuery({ diff --git a/src/common/decorators/docs/mail.decorator.ts b/src/common/decorators/docs/mail.decorator.ts index 2452838..547e222 100644 --- a/src/common/decorators/docs/mail.decorator.ts +++ b/src/common/decorators/docs/mail.decorator.ts @@ -1,8 +1,7 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; import type { MethodNames } from '@/common/types/method'; import type { MailController } from '@/domain/mail/mail.controller'; import { ApiCreatedResponse, ApiOperation } from '@nestjs/swagger'; -import { ApiKudogExceptionResponse } from '../apiKudogExceptionRespone.decorator'; - type MailEndpoints = MethodNames; const MailDocsMap: Record = { diff --git a/src/common/decorators/docs/notice.decorator.ts b/src/common/decorators/docs/notice.decorator.ts index 23e8f4e..cc02a2d 100644 --- a/src/common/decorators/docs/notice.decorator.ts +++ b/src/common/decorators/docs/notice.decorator.ts @@ -8,9 +8,9 @@ import { ApiParam, ApiQuery, } from '@nestjs/swagger'; -import { AddRequestRequestDto } from 'src/domain/notice/dtos/AddRequestRequest.dto'; -import { NoticeInfoResponseDto } from 'src/domain/notice/dtos/NoticeInfoResponse.dto'; -import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; +import { AddRequestRequestDto } from '@/domain/notice/dtos/AddRequestRequest.dto'; +import { NoticeInfoResponseDto } from '@/domain/notice/dtos/NoticeInfoResponse.dto'; +import { NoticeListResponseDto } from '@/domain/notice/dtos/NoticeListResponse.dto'; import { ApiPagination } from './common.decorator'; type NoticeEndpoints = MethodNames; diff --git a/src/common/decorators/docs/notification.decorator.ts b/src/common/decorators/docs/notification.decorator.ts index 18ed4e9..504c388 100644 --- a/src/common/decorators/docs/notification.decorator.ts +++ b/src/common/decorators/docs/notification.decorator.ts @@ -1,4 +1,6 @@ import type { MethodNames } from '@/common/types/method'; +import { NotificationInfoResponseDto } from '@/domain/notification/dtos/noticiationInfoResponse.dto'; +import { TokenRequestDto } from '@/domain/notification/dtos/tokenRequest.dto'; import type { NotificationController } from '@/domain/notification/notification.controller'; import { ApiBody, @@ -7,8 +9,6 @@ import { ApiOperation, ApiQuery, } from '@nestjs/swagger'; -import { NotificationInfoResponseDto } from 'src/domain/notification/dtos/noticiationInfoResponse.dto'; -import { TokenRequestDto } from 'src/domain/notification/dtos/tokenRequest.dto'; import { ApiPagination } from './common.decorator'; type NotificationEndPoints = MethodNames; diff --git a/src/common/decorators/docs/scrap.decorator.ts b/src/common/decorators/docs/scrap.decorator.ts index e77d597..2879593 100644 --- a/src/common/decorators/docs/scrap.decorator.ts +++ b/src/common/decorators/docs/scrap.decorator.ts @@ -1,4 +1,7 @@ import type { MethodNames } from '@/common/types/method'; +import { ScrapBoxRequestDto } from '@/domain/scrap/dtos/scrapBoxRequest.dto'; +import { ScrapBoxResponseDto } from '@/domain/scrap/dtos/scrapBoxResponse.dto'; +import { ScrapBoxResponseWithNotices } from '@/domain/scrap/dtos/scrapBoxResponseWithNotices.dto'; import type { ScrapController } from '@/domain/scrap/scrap.controller'; import { ApiBody, @@ -7,9 +10,6 @@ import { ApiOperation, ApiParam, } from '@nestjs/swagger'; -import { ScrapBoxRequestDto } from 'src/domain/scrap/dtos/scrapBoxRequest.dto'; -import { ScrapBoxResponseDto } from 'src/domain/scrap/dtos/scrapBoxResponse.dto'; -import { ScrapBoxResponseWithNotices } from 'src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto'; import { ApiPagination } from './common.decorator'; type ScrapEndPoints = MethodNames; diff --git a/src/common/decorators/docs/subscribe.decorator.ts b/src/common/decorators/docs/subscribe.decorator.ts index 789ef4c..9bfe9b9 100644 --- a/src/common/decorators/docs/subscribe.decorator.ts +++ b/src/common/decorators/docs/subscribe.decorator.ts @@ -1,4 +1,8 @@ import type { MethodNames } from '@/common/types/method'; +import { NoticeListResponseDto } from '@/domain/notice/dtos/NoticeListResponse.dto'; +import { SubscribeBoxRequestDto } from '@/domain/subscribe/dtos/subscribeBoxRequest.dto'; +import { SubscribeBoxResponseDto } from '@/domain/subscribe/dtos/subscribeBoxResponse.dto'; +import { SubscribeBoxResponseDtoWithNotices } from '@/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto'; import type { SubscribeController } from '@/domain/subscribe/subscribe.controller'; import { ApiBody, @@ -8,10 +12,6 @@ import { ApiParam, ApiQuery, } from '@nestjs/swagger'; -import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; -import { SubscribeBoxRequestDto } from 'src/domain/subscribe/dtos/subscribeBoxRequest.dto'; -import { SubscribeBoxResponseDto } from 'src/domain/subscribe/dtos/subscribeBoxResponse.dto'; -import { SubscribeBoxResponseDtoWithNotices } from 'src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto'; import { ApiPagination } from './common.decorator'; type SubscribeEndPoint = MethodNames; diff --git a/src/common/decorators/docs/user.decorator.ts b/src/common/decorators/docs/user.decorator.ts index f7fefda..e0adfda 100644 --- a/src/common/decorators/docs/user.decorator.ts +++ b/src/common/decorators/docs/user.decorator.ts @@ -1,7 +1,7 @@ import type { MethodNames } from '@/common/types/method'; +import { UserInfoResponseDto } from '@/domain/users/dtos/userInfo.dto'; import type { UsersController } from '@/domain/users/users.controller'; import { ApiOkResponse, ApiOperation } from '@nestjs/swagger'; -import { UserInfoResponseDto } from 'src/domain/users/dtos/userInfo.dto'; type UserEndpoints = MethodNames; diff --git a/src/common/decorators/index.ts b/src/common/decorators/index.ts index 5026273..e323918 100644 --- a/src/common/decorators/index.ts +++ b/src/common/decorators/index.ts @@ -1,5 +1,4 @@ -export * from './injectAccessUser.decorator'; +export * from './injectUser.decorator'; export * from './usePagination.decorator'; -export * from './injectLocalUser.decorator'; -export * from './injectRefreshUser.decorator'; export * from './namedController'; +export * from './apiKudogExceptionResponse.decorator'; diff --git a/src/common/decorators/injectAccessUser.decorator.ts b/src/common/decorators/injectAccessUser.decorator.ts deleted file mode 100644 index d73216b..0000000 --- a/src/common/decorators/injectAccessUser.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { JwtPayload } from '@/common/types/auth'; -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; - -export const InjectAccessUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): JwtPayload => { - const request = cts.switchToHttp().getRequest(); - return request.user; - }, -); diff --git a/src/common/decorators/injectLocalUser.decorator.ts b/src/common/decorators/injectLocalUser.decorator.ts deleted file mode 100644 index c6b46ae..0000000 --- a/src/common/decorators/injectLocalUser.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; - -export const injectLocalUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): number => { - const request = cts.switchToHttp().getRequest(); - return request.user; - }, -); diff --git a/src/common/decorators/injectRefreshUser.decorator.ts b/src/common/decorators/injectRefreshUser.decorator.ts deleted file mode 100644 index dfb3d60..0000000 --- a/src/common/decorators/injectRefreshUser.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RefreshTokenPayload } from '@/common/types/auth'; -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; - -export const InjectRefreshUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): RefreshTokenPayload => { - const request = cts.switchToHttp().getRequest(); - return request.user; - }, -); diff --git a/src/common/decorators/injectUser.decorator.ts b/src/common/decorators/injectUser.decorator.ts new file mode 100644 index 0000000..ebb5057 --- /dev/null +++ b/src/common/decorators/injectUser.decorator.ts @@ -0,0 +1,23 @@ +import { JwtPayload, type RefreshTokenPayload } from '@/common/types/auth'; +import { ExecutionContext, createParamDecorator } from '@nestjs/common'; + +export const InjectAccessUser = createParamDecorator( + (_: unknown, cts: ExecutionContext): JwtPayload => { + const request = cts.switchToHttp().getRequest(); + return request.user; + }, +); + +export const injectLocalUser = createParamDecorator( + (_: unknown, cts: ExecutionContext): number => { + const request = cts.switchToHttp().getRequest(); + return request.user; + }, +); + +export const InjectRefreshUser = createParamDecorator( + (_: unknown, cts: ExecutionContext): RefreshTokenPayload => { + const request = cts.switchToHttp().getRequest(); + return request.user; + }, +); From 8f826c6348edb820a4b280a0ae6ebc651c67b494 Mon Sep 17 00:00:00 2001 From: overthestream Date: Mon, 29 Jul 2024 00:50:03 +0900 Subject: [PATCH 06/11] refactor auth domain --- .github/workflows/cicd.yml | 1 - package.json | 19 +- src/app.module.ts | 9 +- src/asset/error.json | 26 +- src/common/decorators/injectUser.decorator.ts | 17 +- src/common/decorators/namedController.ts | 11 +- src/common/types/auth.ts | 12 +- src/common/types/index.d.ts | 8 + src/common/utils/exception.ts | 2 +- src/domain/auth/auth.controller.ts | 41 +-- src/domain/auth/auth.module.ts | 13 +- src/domain/auth/auth.repository.ts | 93 ++++++ src/domain/auth/auth.service.ts | 259 +++++----------- src/domain/auth/entities/changePwd.entity.ts | 23 +- .../entities/emailAuthentication.entity.ts | 15 +- .../auth/entities/refreshToken.entity.ts | 20 +- src/domain/auth/guards/jwt.guard.ts | 31 ++ .../auth/passport/accessToken.strategy.ts | 23 -- src/domain/auth/passport/local.strategy.ts | 19 -- .../auth/passport/refreshToken.strategy.ts | 33 -- src/domain/users/user.repository.ts | 43 +++ src/domain/users/users.module.ts | 8 +- src/entities/kudogUser.entity.ts | 13 +- src/middlewares/auth.middleware.spec.ts | 81 +++++ src/middlewares/auth.middleware.ts | 42 +++ yarn.lock | 291 ++++++------------ 26 files changed, 595 insertions(+), 558 deletions(-) create mode 100644 src/common/types/index.d.ts create mode 100644 src/domain/auth/auth.repository.ts create mode 100644 src/domain/auth/guards/jwt.guard.ts delete mode 100644 src/domain/auth/passport/accessToken.strategy.ts delete mode 100644 src/domain/auth/passport/local.strategy.ts delete mode 100644 src/domain/auth/passport/refreshToken.strategy.ts create mode 100644 src/domain/users/user.repository.ts create mode 100644 src/middlewares/auth.middleware.spec.ts create mode 100644 src/middlewares/auth.middleware.ts diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 9cc0ea6..70d31f9 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -47,7 +47,6 @@ jobs: echo "MAIL_USER=${{ secrets.MAIL_USER }}" >> .env echo "MAIL_PW=${{ secrets.MAIL_PW }}" >> .env echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env - echo "JWT_REFRESH_SECRET_KEY=${{ secrets.JWT_REFRESH_SECRET_KEY }}" >> .env echo "CHANNEL_API_KEY=${{ secrets.CHANNEL_API_KEY }}" >> .env echo "CHANNEL_API_SECRET=${{ secrets.CHANNEL_API_SECRET }}" >> .env echo "DISCORD_TOKEN=${{secrets.DISCORD_TOKEN}}" >> .env diff --git a/package.json b/package.json index 131e1b6..7e8714d 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,9 @@ "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.1.1", - "@nestjs/passport": "^10.0.2", "@nestjs/platform-express": "^10.0.0", "@nestjs/schedule": "^4.0.0", - "@nestjs/swagger": "^7.1.15", + "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.0", "axios": "^1.6.0", "bcrypt": "^5.1.1", @@ -31,12 +30,10 @@ "domhandler": "^5.0.3", "firebase-admin": "^12.0.0", "htmlparser2": "^9.1.0", + "jsonwebtoken": "^9.0.2", "lodash": "^4.17.21", "nestjs-pino": "^4.1.0", "nodemailer": "^6.9.14", - "passport": "^0.6.0", - "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", "pg": "^8.11.3", "pino": "^9.3.2", "pino-http": "^10.2.0", @@ -53,10 +50,10 @@ "@types/bcrypt": "^5.0.1", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/jsonwebtoken": "^9", "@types/lodash": "^4", "@types/node": "^20.3.1", "@types/nodemailer": "^6", - "@types/passport-local": "^1.0.37", "@types/supertest": "^2.0.12", "biome": "^0.3.3", "jest": "^29.5.0", @@ -70,7 +67,11 @@ "typescript": "^5.1.3" }, "jest": { - "moduleFileExtensions": ["js", "json", "ts"], + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], "rootDir": ".", "moduleNameMapper": { "@/(.*)$": "/src/$1" @@ -79,7 +80,9 @@ "transform": { "^.+\\.(t|j)s$": "ts-jest" }, - "collectCoverageFrom": ["**/*.(t|j)s"], + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" }, diff --git a/src/app.module.ts b/src/app.module.ts index 69cd691..8b5387a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,6 @@ import { randomUUID } from 'node:crypto'; import { MailerModule } from '@nestjs-modules/mailer'; -import { Module } from '@nestjs/common'; +import { type MiddlewareConsumer, Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -18,6 +18,7 @@ import { ScrapModule } from './domain/scrap/scrap.module'; import { SubscribeModule } from './domain/subscribe/subscribe.module'; import { UsersModule } from './domain/users/users.module'; import { FetchModule } from './fetch/fetch.module'; +import { AuthMiddleware } from './middlewares/auth.middleware'; @Module({ imports: [ @@ -85,4 +86,8 @@ import { FetchModule } from './fetch/fetch.module'; controllers: [AppController], providers: [AppService], }) -export class AppModule {} +export class AppModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(AuthMiddleware).forRoutes('*'); + } +} diff --git a/src/asset/error.json b/src/asset/error.json index 9e7e296..34ea501 100644 --- a/src/asset/error.json +++ b/src/asset/error.json @@ -54,37 +54,43 @@ "message": "인증 코드가 만료되었습니다. 다시 인증을 시도해주세요." }, - "LOGIN_FAILED": { "errorCode": 4010, "statusCode": 401, "name": "LOGIN_FAILED", "message": "이메일 또는 비밀번호가 일치하지 않습니다." }, - "TODO_REFRESH": { + "LOGIN_REQUIRED": { "errorCode": 4011, "statusCode": 401, - "name": "TODO_REFRESH", - "message": "할." + "name": "LOGIN_REQUIRED", + "message": "토큰이 없거나 만료되었습니다. 로그인해주세요." }, - "INVALID_ACCESS_TOKEN": { + "JWT_TOKEN_EXPIRED": { "errorCode": 4012, "statusCode": 401, - "name": "INVALID_ACCESS_TOKEN", - "message": "유효하지 않은 토큰입니다." + "name": "JWT_TOKEN_EXPIRED", + "message": "JWT TOKEN이 만료되었습니다. 리프레시를 시도해주세요." }, - "ACCESS_TOKEN_EXPIRED":{ + "JWT_TOKEN_INVALID": { "errorCode": 4013, "statusCode": 401, - "name": "TOKEN_EXPIRED", - "message": "ACCESS TOKEN 만료. 리프레시를 시도해주세요" + "name": "JWT_TOKEN_INVALID", + "message": "JWT TOKEN이 유효하지 않습니다." }, + "USER_NOT_FOUND": { "errorCode": 4040, "statusCode": 404, "name": "USER_NOT_FOUND", "message": "사용자를 찾을 수 없습니다." }, + "EMAIL_NOT_FOUND": { + "errorCode": 4041, + "statusCode": 404, + "name": "EMAIL_NOT_FOUND", + "message": "이메일을 찾을 수 없습니다." + }, "INVALID_PAGE_QUERY": { "errorCode": 4060, diff --git a/src/common/decorators/injectUser.decorator.ts b/src/common/decorators/injectUser.decorator.ts index ebb5057..94a8825 100644 --- a/src/common/decorators/injectUser.decorator.ts +++ b/src/common/decorators/injectUser.decorator.ts @@ -1,23 +1,16 @@ -import { JwtPayload, type RefreshTokenPayload } from '@/common/types/auth'; +import { JwtPayload } from '@/common/types/auth'; import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -export const InjectAccessUser = createParamDecorator( +export const InjectUser = createParamDecorator( (_: unknown, cts: ExecutionContext): JwtPayload => { const request = cts.switchToHttp().getRequest(); return request.user; }, ); -export const injectLocalUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): number => { - const request = cts.switchToHttp().getRequest(); - return request.user; - }, -); - -export const InjectRefreshUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): RefreshTokenPayload => { +export const InjectToken = createParamDecorator( + (_: unknown, cts: ExecutionContext): JwtPayload => { const request = cts.switchToHttp().getRequest(); - return request.user; + return request.headers.authorization.split('Bearer ')[1]; }, ); diff --git a/src/common/decorators/namedController.ts b/src/common/decorators/namedController.ts index 291555b..f5e9e4e 100644 --- a/src/common/decorators/namedController.ts +++ b/src/common/decorators/namedController.ts @@ -1,6 +1,15 @@ import { Controller, applyDecorators } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { ApiKudogExceptionResponse } from './apiKudogExceptionResponse.decorator'; export function NamedController(name: string) { - return applyDecorators(ApiTags(name), Controller(name)); + return applyDecorators( + ApiTags(name), + Controller(name), + ApiKudogExceptionResponse([ + 'JWT_TOKEN_EXPIRED', + 'JWT_TOKEN_INVALID', + 'INTERNAL_SERVER_ERROR', + ]), + ); } diff --git a/src/common/types/auth.ts b/src/common/types/auth.ts index 7b82735..2eacebc 100644 --- a/src/common/types/auth.ts +++ b/src/common/types/auth.ts @@ -1,12 +1,6 @@ -export interface User { +export type User = { id: number; name: string; -} +}; -export interface JwtPayload extends User { - signedAt: string; -} - -export interface RefreshTokenPayload extends JwtPayload { - refreshToken: string; -} +export type JwtPayload = User; diff --git a/src/common/types/index.d.ts b/src/common/types/index.d.ts new file mode 100644 index 0000000..63e4b04 --- /dev/null +++ b/src/common/types/index.d.ts @@ -0,0 +1,8 @@ +import { JwtPayload } from './auth'; +declare global { + namespace Express { + export interface Request { + user?: JwtPayload; + } + } +} diff --git a/src/common/utils/exception.ts b/src/common/utils/exception.ts index b1f0575..1974ae4 100644 --- a/src/common/utils/exception.ts +++ b/src/common/utils/exception.ts @@ -24,6 +24,6 @@ export class HttpException extends Error { } } -export function throwKudogException(name: ExceptionNames) { +export function throwKudogException(name: ExceptionNames): never { throw new HttpException(name); } diff --git a/src/domain/auth/auth.controller.ts b/src/domain/auth/auth.controller.ts index b8d11b7..d14b2f7 100644 --- a/src/domain/auth/auth.controller.ts +++ b/src/domain/auth/auth.controller.ts @@ -1,58 +1,51 @@ -import { - InjectAccessUser, - InjectRefreshUser, - NamedController, - injectLocalUser, -} from '@/common/decorators'; +import { InjectToken, InjectUser, NamedController } from '@/common/decorators'; import { AuthDocs } from '@/common/decorators/docs'; -import { JwtPayload, RefreshTokenPayload } from '@/common/types/auth'; -import { Body, Delete, Post, Put } from '@nestjs/common'; +import { JwtPayload } from '@/common/types/auth'; +import { Body, Delete, Header, Post, Put } from '@nestjs/common'; import { AuthService } from './auth.service'; import { ChangePasswordDto, ChangePasswordRequestDto, VerifyChangePasswordRequestDto, } from './dtos/changePwdRequest.dto'; +import type { LoginRequestDto } from './dtos/loginRequestDto'; import { SignupRequestDto } from './dtos/signupRequest.dto'; import { TokenResponseDto } from './dtos/tokenResponse.dto'; -import { JwtAccessGuard } from './passport/accessToken.strategy'; -import { LocalGuard } from './passport/local.strategy'; -import { JwtRefreshGuard } from './passport/refreshToken.strategy'; +import { UseJwtGuard } from './guards/jwt.guard'; @AuthDocs @NamedController('auth') export class AuthController { constructor(private readonly authService: AuthService) {} - @LocalGuard() @Post('/login') - async login(@injectLocalUser() user: number): Promise { - return this.authService.getToken(user); + async login(@Body() body: LoginRequestDto): Promise { + return this.authService.login(body); } @Post('/signup') async signup(@Body() body: SignupRequestDto): Promise { - const id = await this.authService.signup(body); - return this.authService.getToken(id); + return this.authService.signup(body); } - @JwtRefreshGuard() + @UseJwtGuard() @Post('/refresh') async refresh( - @InjectRefreshUser() user: RefreshTokenPayload, + @InjectUser() user: JwtPayload, + @InjectToken() token: string, ): Promise { - return this.authService.refreshJWT(user); + return this.authService.refreshJWT(user, token); } - @JwtRefreshGuard() + @UseJwtGuard() @Delete('/logout') - async logout(@InjectRefreshUser() user: RefreshTokenPayload): Promise { - return this.authService.logout(user); + async logout(@InjectToken() token: string): Promise { + return this.authService.logout(token); } - @JwtAccessGuard() + @UseJwtGuard() @Delete('/user-info') - async deleteUser(@InjectAccessUser() user: JwtPayload): Promise { + async deleteUser(@InjectUser() user: JwtPayload): Promise { return this.authService.deleteUser(user.id); } diff --git a/src/domain/auth/auth.module.ts b/src/domain/auth/auth.module.ts index 8a00ce2..0f82bc7 100644 --- a/src/domain/auth/auth.module.ts +++ b/src/domain/auth/auth.module.ts @@ -6,28 +6,25 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ChangePwdAuthenticationEntity, EmailAuthenticationEntity, - KudogUser, RefreshTokenEntity, } from 'src/entities'; +import { UsersModule } from '../users/users.module'; import { AuthController } from './auth.controller'; +import { AuthRepository } from './auth.repository'; import { AuthService } from './auth.service'; -import { JwtStrategy as AccessStrategy } from './passport/accessToken.strategy'; -import { LocalStrategy } from './passport/local.strategy'; -import { JwtStrategy as RefreshStrategy } from './passport/refreshToken.strategy'; - @Module({ imports: [ TypeOrmModule.forFeature([ - KudogUser, ChangePwdAuthenticationEntity, EmailAuthenticationEntity, RefreshTokenEntity, ]), - MailerModule, JwtModule.register({}), + UsersModule, + MailerModule, ChannelModule, ], controllers: [AuthController], - providers: [AuthService, RefreshStrategy, AccessStrategy, LocalStrategy], + providers: [AuthService, AuthRepository], }) export class AuthModule {} diff --git a/src/domain/auth/auth.repository.ts b/src/domain/auth/auth.repository.ts new file mode 100644 index 0000000..87c0c6f --- /dev/null +++ b/src/domain/auth/auth.repository.ts @@ -0,0 +1,93 @@ +import { + ChangePwdAuthenticationEntity, + EmailAuthenticationEntity, + RefreshTokenEntity, +} from '@/entities'; +import { Injectable } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { type DataSource, LessThan, Repository } from 'typeorm'; + +@Injectable() +export class AuthRepository { + private changePwdAuthEntityRepository: Repository; + private emailAuthEntityRepository: Repository; + private refreshTokenEntityRepository: Repository; + constructor(@InjectDataSource() private dataSource: DataSource) { + this.changePwdAuthEntityRepository = this.dataSource.getRepository( + ChangePwdAuthenticationEntity, + ); + this.emailAuthEntityRepository = this.dataSource.getRepository( + EmailAuthenticationEntity, + ); + this.refreshTokenEntityRepository = + this.dataSource.getRepository(RefreshTokenEntity); + } + + async findValidToken( + token: string, + id: number, + ): Promise { + return this.refreshTokenEntityRepository.findOne({ + where: { token, userId: id }, + }); + } + + async insertToken(token: string, userId: number): Promise { + await this.refreshTokenEntityRepository.insert({ userId, token }); + } + + async findNewestEmailAuth(email: string): Promise { + return this.emailAuthEntityRepository.findOne({ + where: { email }, + order: { createdAt: 'DESC' }, + }); + } + + async findNewestChangePwdAuthByUserId( + userId: number, + ): Promise { + return this.changePwdAuthEntityRepository.findOne({ + where: { userId }, + order: { createdAt: 'DESC' }, + }); + } + + async findChangePwdAuthByCode( + code: string, + ): Promise { + return this.changePwdAuthEntityRepository.findOne({ where: { code } }); + } + + async findByToken(token: string): Promise { + return this.refreshTokenEntityRepository.findOne({ where: { token } }); + } + + async removeToken(token: RefreshTokenEntity): Promise { + await this.refreshTokenEntityRepository.remove(token); + } + + async insertChangePwdAuth(userId: number, code: string) { + await this.changePwdAuthEntityRepository.insert({ userId, code }); + } + + async authenticatePwdCode(entity: ChangePwdAuthenticationEntity) { + entity.authenticated = true; + await this.changePwdAuthEntityRepository.save(entity); + } + + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async deleteExpiredEntities() { + this.changePwdAuthEntityRepository.delete({ + createdAt: LessThan(new Date(Date.now() - 60 * 60 * 1000)), + }); + + this.emailAuthEntityRepository.delete({ + createdAt: LessThan(new Date(Date.now() - 60 * 60 * 1000)), + }); + + this.refreshTokenEntityRepository.delete({ + createdAt: LessThan(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)), + }); + } +} diff --git a/src/domain/auth/auth.service.ts b/src/domain/auth/auth.service.ts index 09022a0..4873d52 100644 --- a/src/domain/auth/auth.service.ts +++ b/src/domain/auth/auth.service.ts @@ -1,115 +1,79 @@ -import { JwtPayload, RefreshTokenPayload } from '@/common/types/auth'; +import { JwtPayload } from '@/common/types/auth'; +import { throwKudogException } from '@/common/utils/exception'; import { ChannelService } from '@/domain/channel/channel.service'; import { MailerService } from '@nestjs-modules/mailer'; -import { - BadRequestException, - HttpException, - HttpStatus, - Injectable, - NotFoundException, - RequestTimeoutException, - UnauthorizedException, -} from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import { InjectRepository } from '@nestjs/typeorm'; import { compare, hash } from 'bcrypt'; -import { - ChangePwdAuthenticationEntity, - EmailAuthenticationEntity, - KudogUser, - RefreshTokenEntity, -} from 'src/entities'; -import { Repository } from 'typeorm'; +import { UserRepository } from '../users/user.repository'; +import { AuthRepository } from './auth.repository'; import { ChangePasswordDto, ChangePasswordRequestDto, VerifyChangePasswordRequestDto, } from './dtos/changePwdRequest.dto'; +import type { LoginRequestDto } from './dtos/loginRequestDto'; import { SignupRequestDto } from './dtos/signupRequest.dto'; import { TokenResponseDto } from './dtos/tokenResponse.dto'; @Injectable() export class AuthService { constructor( - @InjectRepository(KudogUser) - private readonly userRepository: Repository, - @InjectRepository(ChangePwdAuthenticationEntity) - private readonly changePwdAuthRepository: Repository, - @InjectRepository(EmailAuthenticationEntity) - private readonly emailAuthenticationRepository: Repository, - @InjectRepository(RefreshTokenEntity) - private readonly refreshTokenRepository: Repository, private jwtService: JwtService, private readonly mailerService: MailerService, private readonly channelService: ChannelService, + private readonly userRepository: UserRepository, + private readonly authRepository: AuthRepository, ) {} saltOrRounds = 10; async deleteUser(id: number): Promise { - const user = await this.userRepository.findOne({ - where: { id }, - }); - if (!user) throw new NotFoundException('존재하지 않는 유저입니다.'); + const user = await this.userRepository.findById(id); + + if (!user) throwKudogException('USER_NOT_FOUND'); await this.userRepository.remove(user); } - async validateUser(email: string, password: string): Promise { - const user = await this.userRepository.findOne({ - where: { email }, - }); + private async validateUser( + email: string, + password: string, + ): Promise { + const user = await this.userRepository.findByEmail(email); if (!user) { - throw new UnauthorizedException( - '이메일 또는 비밀번호가 일치하지 않습니다.', - ); + throwKudogException('LOGIN_FAILED'); } const passwordMatch = await compare(password, user.passwordHash); if (!passwordMatch) { - throw new UnauthorizedException( - '이메일 또는 비밀번호가 일치하지 않습니다.', - ); + throwKudogException('LOGIN_FAILED'); } - return user.id; + return { id: user.id, name: user.name }; } - async refreshJWT(payload: RefreshTokenPayload): Promise { - const refreshToken = await this.refreshTokenRepository.findOne({ - where: { token: payload.refreshToken, user: { id: payload.id } }, - }); - if (!refreshToken) - throw new UnauthorizedException('존재하지 않는 유저입니다.'); + async refreshJWT( + payload: JwtPayload, + token: string, + ): Promise { + const refreshToken = await this.authRepository.findValidToken( + token, + payload.id, + ); + if (!refreshToken) throwKudogException('LOGIN_REQUIRED'); + await this.authRepository.removeToken(refreshToken); + return this.getToken(payload); + } - const newPayload: JwtPayload = { - id: payload.id, - name: payload.name, - signedAt: Date.now().toString(), - }; - const accessToken = this.jwtService.sign(newPayload, { - expiresIn: '1h', - secret: process.env.JWT_SECRET_KEY, - }); + async login(loginInfo: LoginRequestDto): Promise { + const { email, password } = loginInfo; - const newRefreshToken = this.jwtService.sign(newPayload, { - expiresIn: '30d', - secret: process.env.JWT_REFRESH_SECRET_KEY, - }); - refreshToken.token = newRefreshToken; + const payload = await this.validateUser(email, password); - await this.refreshTokenRepository.save(refreshToken); - return new TokenResponseDto(accessToken, newRefreshToken); + return this.getToken(payload); } - async getToken(id: number): Promise { - const user = await this.userRepository.findOne({ where: { id } }); - if (!user) throw new UnauthorizedException('존재하지 않는 유저입니다.'); - - const payload: JwtPayload = { - id, - name: user.name, - signedAt: Date.now().toString(), - }; + private async getToken(payload: JwtPayload): Promise { const accessToken = this.jwtService.sign(payload, { - expiresIn: '1h', + expiresIn: '30m', secret: process.env.JWT_SECRET_KEY, }); @@ -117,87 +81,56 @@ export class AuthService { expiresIn: '30d', secret: process.env.JWT_REFRESH_SECRET_KEY, }); - await this.refreshTokenRepository.insert({ - token: refreshToken, - user, - }); + await this.authRepository.insertToken(refreshToken, payload.id); return new TokenResponseDto(accessToken, refreshToken); } - async signup(signupInfo: SignupRequestDto): Promise { + async signup(signupInfo: SignupRequestDto): Promise { const { password, name, email } = signupInfo; - if (!email || !password || !name) - throw new BadRequestException('필수 정보를 입력해주세요.'); - if (!email.endsWith('@korea.ac.kr')) - throw new BadRequestException('korea.ac.kr 이메일이 아닙니다.'); - if (!/^[a-z0-9]{6,16}$/.test(password)) - throw new BadRequestException( - '비밀번호는 6~16자의 영문 소문자와 숫자로만 입력해주세요.', - ); - - const existingUser = await this.userRepository.findOne({ - where: { - email, - }, - }); - if (existingUser) throw new BadRequestException('사용중인 이메일입니다.'); - - const mailAuthentication = await this.emailAuthenticationRepository.findOne( - { - where: { - email, - }, - order: { createdAt: 'DESC' }, - }, - ); + + const existingUser = await this.userRepository.findByEmail(email); + if (existingUser) throwKudogException('EMAIL_ALREADY_USED'); + + const mailAuthentication = + await this.authRepository.findNewestEmailAuth(email); + if (!mailAuthentication || mailAuthentication.authenticated !== true) - throw new BadRequestException('인증되지 않은 이메일입니다.'); + throwKudogException('EMAIL_NOT_VALIDATED'); if (mailAuthentication.createdAt.getTime() + 1000 * 60 * 10 < Date.now()) - throw new RequestTimeoutException( - '인증 후 너무 오랜 시간이 지났습니다. 다시 인증해주세요.', - ); + throwKudogException('EMAIL_VALIDATION_EXPIRED'); + const passwordHash = await hash(password, this.saltOrRounds); - const user = this.userRepository.create({ - email, - name, - passwordHash, - }); - await this.userRepository.save(user); + + const user = await this.userRepository.insert(email, name, passwordHash); const userCount = await this.userRepository.count(); this.channelService.sendMessageToKudog(`가입자 수 ${userCount}명 돌파🔥`); - return user.id; + const payload = { + id: user.id, + name: user.name, + }; + return this.getToken(payload); } async changePwdRequest(dto: ChangePasswordRequestDto): Promise { const { email } = dto; - if (!email || !email.endsWith('@korea.ac.kr')) - throw new BadRequestException('korea.ac.kr 이메일을 입력하세요.'); - const user = await this.userRepository.findOne({ - where: { email }, - }); + const user = await this.userRepository.findByEmail(email); - if (!user) - throw new NotFoundException('해당 이메일의 유저가 존재하지 않습니다.'); + if (!user) throwKudogException('EMAIL_NOT_FOUND'); - const existingEntity = await this.changePwdAuthRepository.findOne({ - where: { user: { id: user.id } }, - }); + const existingEntity = + await this.authRepository.findNewestChangePwdAuthByUserId(user.id); if ( existingEntity && existingEntity.createdAt.getTime() + 1000 * 10 > new Date().getTime() ) - throw new HttpException( - '잠시 후에 다시 시도해주세요', - HttpStatus.TOO_MANY_REQUESTS, - ); - - if (existingEntity) - await this.changePwdAuthRepository.remove(existingEntity); + throwKudogException('TOO_MANY_REQUESTS'); - const code = Math.floor(Math.random() * 1000000).toString(); + const code = Math.floor(Math.random() * 1000000) + .toString() + .padStart(6, '0'); try { await this.mailerService.sendMail({ from: process.env.MAIL_USER, @@ -206,72 +139,48 @@ export class AuthService { html: `인증 번호 ${code}를 입력해주세요.`, }); } catch (err) { - throw new HttpException( - '알 수 없는 이유로 메일 전송에 실패했습니다. 잠시 후에 다시 시도해주세요.', - 510, - ); + throwKudogException('EMAIL_SEND_FAILED'); } - const entity = this.changePwdAuthRepository.create({ - user, - code, - }); - await this.changePwdAuthRepository.save(entity); + await this.authRepository.insertChangePwdAuth(user.id, code); } - async logout(payload: RefreshTokenPayload): Promise { - const token = await this.refreshTokenRepository.findOne({ - where: { user: { id: payload.id }, token: payload.refreshToken }, - }); - if (!token) throw new NotFoundException('존재하지 않는 유저입니다.'); + async logout(refreshToken: string): Promise { + const token = await this.authRepository.findByToken(refreshToken); + if (!token) throwKudogException('JWT_TOKEN_INVALID'); - await this.refreshTokenRepository.remove(token); + await this.authRepository.removeToken(token); } async verifyChangePwdCode( dto: VerifyChangePasswordRequestDto, ): Promise { const { code } = dto; - const entity = await this.changePwdAuthRepository.findOne({ - where: { code }, - order: { createdAt: 'DESC' }, - }); + const entity = await this.authRepository.findChangePwdAuthByCode(code); - if (!entity) - throw new BadRequestException('인증 코드가 일치하지 않습니다.'); - if (entity.createdAt.getTime() + 1000 * 60 * 3 < new Date().getTime()) { - await this.changePwdAuthRepository.remove(entity); - throw new RequestTimeoutException( - '인증 요청 이후 3분이 지났습니다. 다시 메일 전송을 해주세요.', - ); - } + if (!entity) throwKudogException('CODE_NOT_CORRECT'); - entity.expireAt = new Date(new Date().getTime() + 1000 * 60 * 10); - entity.authenticated = true; - await this.changePwdAuthRepository.save(entity); + if (entity.createdAt.getTime() + 1000 * 60 * 3 < Date.now()) + throwKudogException('CODE_EXPIRED'); + await this.authRepository.authenticatePwdCode(entity); } async changePassword(dto: ChangePasswordDto): Promise { const { email, password } = dto; - const user = await this.userRepository.findOne({ - where: { email }, - }); - if (!user) throw new NotFoundException('존재하지 않는 유저입니다.'); + const user = await this.userRepository.findByEmail(email); + if (!user) throwKudogException('USER_NOT_FOUND'); - const entity = await this.changePwdAuthRepository.findOne({ - where: { user }, - }); + const entity = await this.authRepository.findNewestChangePwdAuthByUserId( + user.id, + ); if (!entity || !entity.authenticated) - throw new UnauthorizedException('인증 코드 인증이 완료되지 않았습니다.'); + throwKudogException('CODE_NOT_VALIDATED'); - if (entity.expireAt.getTime() + 1000 * 60 * 10 < new Date().getTime()) { - throw new RequestTimeoutException( - '인증 이후 10분이 지났습니다. 다시 메일 전송을 해주세요.', - ); + if (entity.createdAt.getTime() + 1000 * 60 * 10 < Date.now()) { + throwKudogException('CODE_VALIDATION_EXPIRED'); } const passwordHash = await hash(password, this.saltOrRounds); - user.passwordHash = passwordHash; - await this.userRepository.save(user); + await this.userRepository.changePwd(user, passwordHash); } } diff --git a/src/domain/auth/entities/changePwd.entity.ts b/src/domain/auth/entities/changePwd.entity.ts index 88b6625..6fc4334 100644 --- a/src/domain/auth/entities/changePwd.entity.ts +++ b/src/domain/auth/entities/changePwd.entity.ts @@ -2,34 +2,37 @@ import { Column, CreateDateColumn, Entity, + Index, JoinColumn, + ManyToOne, OneToOne, PrimaryGeneratedColumn, + RelationId, } from 'typeorm'; import { KudogUser } from '../../../entities/kudogUser.entity'; @Entity('change_pwd_authentication') export class ChangePwdAuthenticationEntity { @PrimaryGeneratedColumn() - id: number; + id!: number; - @OneToOne( + @ManyToOne( () => KudogUser, () => undefined, { onDelete: 'CASCADE' }, ) - @JoinColumn() - user: KudogUser; + @JoinColumn({ name: 'userId' }) + user!: KudogUser; + @RelationId((entity: ChangePwdAuthenticationEntity) => entity.user) + userId!: number; @CreateDateColumn() - createdAt: Date; - - @Column({ nullable: true }) - expireAt?: Date; + createdAt!: Date; @Column({ default: false }) - authenticated: boolean; + authenticated!: boolean; + @Index() @Column() - code: string; + code!: string; } diff --git a/src/domain/auth/entities/emailAuthentication.entity.ts b/src/domain/auth/entities/emailAuthentication.entity.ts index 63721fb..a687879 100644 --- a/src/domain/auth/entities/emailAuthentication.entity.ts +++ b/src/domain/auth/entities/emailAuthentication.entity.ts @@ -2,26 +2,25 @@ import { Column, CreateDateColumn, Entity, + Index, PrimaryGeneratedColumn, } from 'typeorm'; @Entity('email_authentication') export class EmailAuthenticationEntity { @PrimaryGeneratedColumn() - id: number; + id!: number; + @Index() @Column() - email: string; + email!: string; @CreateDateColumn() - createdAt: Date; - - @Column({ nullable: true }) - expireAt?: Date; + createdAt!: Date; @Column({ default: false }) - authenticated: boolean; + authenticated!: boolean; @Column() - code: string; + code!: string; } diff --git a/src/domain/auth/entities/refreshToken.entity.ts b/src/domain/auth/entities/refreshToken.entity.ts index 62c4fba..c42cfa3 100644 --- a/src/domain/auth/entities/refreshToken.entity.ts +++ b/src/domain/auth/entities/refreshToken.entity.ts @@ -1,17 +1,19 @@ import { KudogUser } from 'src/entities'; import { - Entity, Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId, - JoinColumn, } from 'typeorm'; @Entity('refresh_token') export class RefreshTokenEntity { @PrimaryGeneratedColumn() - id: number; + id!: number; @ManyToOne( () => KudogUser, @@ -21,11 +23,15 @@ export class RefreshTokenEntity { }, ) @JoinColumn({ name: 'userId' }) - user: KudogUser; + user!: KudogUser; @RelationId((refreshToken: RefreshTokenEntity) => refreshToken.user) - userId: number; + userId!: number; + + @Index() + @Column({ type: 'varchar', length: 255 }) + token!: string; - @Column() - token: string; + @CreateDateColumn() + createdAt!: Date; } diff --git a/src/domain/auth/guards/jwt.guard.ts b/src/domain/auth/guards/jwt.guard.ts new file mode 100644 index 0000000..2622c28 --- /dev/null +++ b/src/domain/auth/guards/jwt.guard.ts @@ -0,0 +1,31 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; +import type { JwtPayload } from '@/common/types/auth'; +import { throwKudogException } from '@/common/utils/exception'; +import { + type CanActivate, + type ExecutionContext, + Injectable, + UseGuards, + applyDecorators, +} from '@nestjs/common'; +import type { Request } from 'express'; +import type { Observable } from 'rxjs'; + +@Injectable() +class JwtGuard implements CanActivate { + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + const user = request.user; + if (user?.id && user?.name) return true; + throwKudogException('LOGIN_REQUIRED'); + } +} + +export function UseJwtGuard() { + return applyDecorators( + UseGuards(JwtGuard), + ApiKudogExceptionResponse(['LOGIN_REQUIRED']), + ); +} diff --git a/src/domain/auth/passport/accessToken.strategy.ts b/src/domain/auth/passport/accessToken.strategy.ts deleted file mode 100644 index 50dcb4b..0000000 --- a/src/domain/auth/passport/accessToken.strategy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { JwtPayload } from '@/common/types/auth'; -import { Injectable, UseGuards, applyDecorators } from '@nestjs/common'; -import { AuthGuard, PassportStrategy } from '@nestjs/passport'; -import { ExtractJwt, Strategy } from 'passport-jwt'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-access') { - constructor() { - super({ - secretOrKey: process.env.JWT_SECRET_KEY, - ignoreExpiration: false, - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - }); - } - - async validate(payload: JwtPayload): Promise { - return payload; - } -} - -export function JwtAccessGuard() { - return UseGuards(AuthGuard('jwt-access')); -} diff --git a/src/domain/auth/passport/local.strategy.ts b/src/domain/auth/passport/local.strategy.ts deleted file mode 100644 index f58ebae..0000000 --- a/src/domain/auth/passport/local.strategy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable, UseGuards } from '@nestjs/common'; -import { AuthGuard, PassportStrategy } from '@nestjs/passport'; -import { Strategy } from 'passport-local'; -import { AuthService } from '../auth.service'; - -@Injectable() -export class LocalStrategy extends PassportStrategy(Strategy, 'local') { - constructor(private authService: AuthService) { - super({ usernameField: 'email' }); - } - - async validate(email: string, password: string) { - return await this.authService.validateUser(email, password); - } -} - -export function LocalGuard() { - return UseGuards(AuthGuard('local')); -} diff --git a/src/domain/auth/passport/refreshToken.strategy.ts b/src/domain/auth/passport/refreshToken.strategy.ts deleted file mode 100644 index 4553de6..0000000 --- a/src/domain/auth/passport/refreshToken.strategy.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { JwtPayload, RefreshTokenPayload } from '@/common/types/auth'; -import { Injectable, UseGuards } from '@nestjs/common'; -import { AuthGuard, PassportStrategy } from '@nestjs/passport'; -import { Request } from 'express'; -import { ExtractJwt, Strategy } from 'passport-jwt'; - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy, 'jwt-refresh') { - constructor() { - super({ - secretOrKey: process.env.JWT_REFRESH_SECRET_KEY, - ignoreExpiration: false, - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - passReqToCallback: true, - }); - } - - async validate( - req: Request, - payload: JwtPayload, - ): Promise { - const refreshToken = req.get('authorization').split('Bearer ')[1]; - - return { - ...payload, - refreshToken, - }; - } -} - -export function JwtRefreshGuard() { - return UseGuards(AuthGuard('jwt-refresh')); -} diff --git a/src/domain/users/user.repository.ts b/src/domain/users/user.repository.ts new file mode 100644 index 0000000..980f361 --- /dev/null +++ b/src/domain/users/user.repository.ts @@ -0,0 +1,43 @@ +import { KudogUser } from '@/entities'; +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { type DataSource, type FindManyOptions, Repository } from 'typeorm'; + +@Injectable() +export class UserRepository { + private entityRepository: Repository; + + constructor(@InjectDataSource() private dataSource: DataSource) { + this.entityRepository = this.dataSource.getRepository(KudogUser); + } + + async findById(id: number): Promise { + return this.entityRepository.findOne({ where: { id } }); + } + + async findByEmail(email: string): Promise { + return this.entityRepository.findOne({ where: { email } }); + } + + async remove(user: KudogUser): Promise { + return this.entityRepository.remove(user); + } + + async insert( + email: string, + name: string, + passwordHash: string, + ): Promise { + const entity = this.entityRepository.create({ email, name, passwordHash }); + return this.entityRepository.save(entity); + } + + async count(options?: FindManyOptions): Promise { + return this.entityRepository.count(options); + } + + async changePwd(entity: KudogUser, passwordHash: string): Promise { + entity.passwordHash = passwordHash; + await this.entityRepository.save(entity); + } +} diff --git a/src/domain/users/users.module.ts b/src/domain/users/users.module.ts index f3a6d79..8b5e085 100644 --- a/src/domain/users/users.module.ts +++ b/src/domain/users/users.module.ts @@ -1,12 +1,14 @@ import { Module } from '@nestjs/common'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { KudogUser, ProviderBookmark } from 'src/entities'; +import { UserRepository } from './user.repository'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; @Module({ controllers: [UsersController], - providers: [UsersService], + providers: [UsersService, UserRepository], + exports: [UserRepository], imports: [TypeOrmModule.forFeature([KudogUser, ProviderBookmark])], }) export class UsersModule {} diff --git a/src/entities/kudogUser.entity.ts b/src/entities/kudogUser.entity.ts index 71a9bf6..9b572de 100644 --- a/src/entities/kudogUser.entity.ts +++ b/src/entities/kudogUser.entity.ts @@ -1,11 +1,17 @@ -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { - ScrapBoxEntity, - SubscribeBoxEntity, NotificationEntity, NotificationTokenEntity, RefreshTokenEntity, + ScrapBoxEntity, + SubscribeBoxEntity, } from 'src/entities'; +import { + Column, + Entity, + Index, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; import { ProviderBookmark } from './providerBookmark.entity'; @Entity('kudog_user') @@ -13,6 +19,7 @@ export class KudogUser { @PrimaryGeneratedColumn() id: number; + @Index() @Column() email: string; diff --git a/src/middlewares/auth.middleware.spec.ts b/src/middlewares/auth.middleware.spec.ts new file mode 100644 index 0000000..fb6de85 --- /dev/null +++ b/src/middlewares/auth.middleware.spec.ts @@ -0,0 +1,81 @@ +import { type Request, Response } from 'express'; +import * as jwt from 'jsonwebtoken'; + +import type { JwtPayload } from '@/common/types/auth'; +import { AuthMiddleware } from './auth.middleware'; + +describe('authMiddleware', () => { + const authMiddleware = new AuthMiddleware(); + process.env.JWT_SECRET_KEY = 'secret'; + + it('use fake-auth header not in prod', async () => { + const mockRequest = { + headers: { + 'x-fake-auth': '123', + }, + user: { + id: 0, + }, + }; + + const mockNext = jest.fn(); + process.env.NODE_ENV = 'dev'; + await authMiddleware.use( + mockRequest as unknown as Request, + {} as Response, + mockNext, + ); + expect(mockRequest.user.id).toEqual(123); + }); + + it('not use fake-auth in production', async () => { + const mockRequest = { + headers: { + 'x-fake-auth': '123', + }, + user: { + id: 0, + }, + }; + + const mockNext = jest.fn(); + process.env.NODE_ENV = 'production'; + await authMiddleware.use( + mockRequest as unknown as Request, + {} as Response, + mockNext, + ); + + expect(mockRequest.user.id).toEqual(0); + }); + + it('expired token must throw JWT EXPIRED', async () => { + const { JWT_SECRET_KEY } = process.env; + const payload: JwtPayload = { + id: 1, + name: 'test', + signedAt: Date.now().toString(), + }; + const options = { expiresIn: -200 }; + const token = jwt.sign(payload, JWT_SECRET_KEY, options); + const mockRequest = { + headers: { + authorization: `Bearer ${token}`, + }, + }; + + const mockNext = jest.fn(); + + process.env.NODE_ENV = 'production'; + + try { + await authMiddleware.use( + mockRequest as unknown as Request, + {} as Response, + mockNext, + ); + } catch (err) { + expect(err.message).toEqual('JWT_TOKEN_EXPIRED'); + } + }); +}); diff --git a/src/middlewares/auth.middleware.ts b/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..a744930 --- /dev/null +++ b/src/middlewares/auth.middleware.ts @@ -0,0 +1,42 @@ +import type { JwtPayload } from '@/common/types/auth'; +import { throwKudogException } from '@/common/utils/exception'; +import { Injectable, type NestMiddleware } from '@nestjs/common'; +import type { NextFunction, Request, Response } from 'express'; +import * as jwt from 'jsonwebtoken'; + +@Injectable() +export class AuthMiddleware implements NestMiddleware { + async use(req: Request, res: Response, next: NextFunction) { + const { JWT_SECRET_KEY } = process.env; + if (!JWT_SECRET_KEY) { + throwKudogException('INTERNAL_SERVER_ERROR'); + } + if ( + process.env.NODE_ENV !== 'production' && + req.headers['x-fake-auth'] !== undefined + ) { + req.user = { + id: Number.parseInt(req.headers['x-fake-auth'] as string), + name: 'fake-auth', + }; + } else if (req.headers.authorization) { + const [, token] = req.headers.authorization.split('Bearer '); + + if (!token) { + try { + req.user = jwt.verify(token, JWT_SECRET_KEY) as JwtPayload; + } catch (err) { + if (err.name === 'TokenExpiredError') { + throwKudogException('JWT_TOKEN_EXPIRED'); + } + if (err.name === 'JsonWebTokenError') { + throwKudogException('JWT_TOKEN_INVALID'); + } + throwKudogException('INTERNAL_SERVER_ERROR'); + } + } + } + + return next(); + } +} diff --git a/yarn.lock b/yarn.lock index e8281d6..af44bb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -74,9 +74,9 @@ __metadata: linkType: hard "@babel/compat-data@npm:^7.24.8": - version: 7.24.9 - resolution: "@babel/compat-data@npm:7.24.9" - checksum: 10c0/95a69c9ed00ae78b4921f33403e9b35518e6139a0c46af763c65dea160720cb57c6cc23f7d30249091a0248335b0e39de5c8dfa8e7877c830e44561e0bdc1254 + version: 7.25.0 + resolution: "@babel/compat-data@npm:7.25.0" + checksum: 10c0/2873df153aa0c60f9e63369320beb5fd9ca948552a06c77db1eb0687bd10a296c9fbf9996bd4b3c8137a78eba3a0f0edfc41b65f57fca8421e5c0c8bb13a813d languageName: node linkType: hard @@ -103,15 +103,15 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.24.8, @babel/generator@npm:^7.24.9, @babel/generator@npm:^7.7.2": - version: 7.24.10 - resolution: "@babel/generator@npm:7.24.10" +"@babel/generator@npm:^7.24.9, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.7.2": + version: 7.25.0 + resolution: "@babel/generator@npm:7.25.0" dependencies: - "@babel/types": "npm:^7.24.9" + "@babel/types": "npm:^7.25.0" "@jridgewell/gen-mapping": "npm:^0.3.5" "@jridgewell/trace-mapping": "npm:^0.3.25" jsesc: "npm:^2.5.1" - checksum: 10c0/abcfd75f625aecc87ce6036ef788b12723fd3c46530df1130d1f00d18e48b462849ddaeef8b1a02bfdcb6e28956389a98c5729dad1c3c5448307dacb6c959f29 + checksum: 10c0/d0e2dfcdc8bdbb5dded34b705ceebf2e0bc1b06795a1530e64fb6a3ccf313c189db7f60c1616effae48114e1a25adc75855bc4496f3779a396b3377bae718ce7 languageName: node linkType: hard @@ -128,34 +128,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-environment-visitor@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-environment-visitor@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - checksum: 10c0/36ece78882b5960e2d26abf13cf15ff5689bf7c325b10a2895a74a499e712de0d305f8d78bb382dd3c05cfba7e47ec98fe28aab5674243e0625cd38438dd0b2d - languageName: node - linkType: hard - -"@babel/helper-function-name@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-function-name@npm:7.24.7" - dependencies: - "@babel/template": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10c0/e5e41e6cf86bd0f8bf272cbb6e7c5ee0f3e9660414174435a46653efba4f2479ce03ce04abff2aa2ef9359cf057c79c06cb7b134a565ad9c0e8a50dcdc3b43c4 - languageName: node - linkType: hard - -"@babel/helper-hoist-variables@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-hoist-variables@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - checksum: 10c0/19ee37563bbd1219f9d98991ad0e9abef77803ee5945fd85aa7aa62a67c69efca9a801696a1b58dda27f211e878b3327789e6fd2a6f6c725ccefe36774b5ce95 - languageName: node - linkType: hard - "@babel/helper-module-imports@npm:^7.24.7": version: 7.24.7 resolution: "@babel/helper-module-imports@npm:7.24.7" @@ -167,17 +139,16 @@ __metadata: linkType: hard "@babel/helper-module-transforms@npm:^7.24.9": - version: 7.24.9 - resolution: "@babel/helper-module-transforms@npm:7.24.9" + version: 7.25.0 + resolution: "@babel/helper-module-transforms@npm:7.25.0" dependencies: - "@babel/helper-environment-visitor": "npm:^7.24.7" "@babel/helper-module-imports": "npm:^7.24.7" "@babel/helper-simple-access": "npm:^7.24.7" - "@babel/helper-split-export-declaration": "npm:^7.24.7" "@babel/helper-validator-identifier": "npm:^7.24.7" + "@babel/traverse": "npm:^7.25.0" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10c0/e27bca43bc113731ee4f2b33a4c5bf9c7eebf4d64487b814c305cbd5feb272c29fcd3d79634ba03131ade171e5972bc7ede8dbc83ba0deb02f1e62d318c87770 + checksum: 10c0/83c0ea9bbd10afbf3539c40ff2c255dd9af6a003dd4a51ed94faed110a52a0ab510fcdd7a675117e8b72d6b479643864674b9243997516c8d77a95dd688e0c9a languageName: node linkType: hard @@ -198,15 +169,6 @@ __metadata: languageName: node linkType: hard -"@babel/helper-split-export-declaration@npm:^7.24.7": - version: 7.24.7 - resolution: "@babel/helper-split-export-declaration@npm:7.24.7" - dependencies: - "@babel/types": "npm:^7.24.7" - checksum: 10c0/0254577d7086bf09b01bbde98f731d4fcf4b7c3fa9634fdb87929801307c1f6202a1352e3faa5492450fa8da4420542d44de604daf540704ff349594a78184f6 - languageName: node - linkType: hard - "@babel/helper-string-parser@npm:^7.24.8": version: 7.24.8 resolution: "@babel/helper-string-parser@npm:7.24.8" @@ -229,12 +191,12 @@ __metadata: linkType: hard "@babel/helpers@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/helpers@npm:7.24.8" + version: 7.25.0 + resolution: "@babel/helpers@npm:7.25.0" dependencies: - "@babel/template": "npm:^7.24.7" - "@babel/types": "npm:^7.24.8" - checksum: 10c0/42b8939b0a0bf72d6df9721973eb0fd7cd48f42641c5c9c740916397faa586255c06d36c6e6a7e091860723096281c620f6ffaee0011a3bb254a6f5475d89a12 + "@babel/template": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" + checksum: 10c0/b7fe007fc4194268abf70aa3810365085e290e6528dcb9fbbf7a765d43c74b6369ce0f99c5ccd2d44c413853099daa449c9a0123f0b212ac8d18643f2e8174b8 languageName: node linkType: hard @@ -250,12 +212,12 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.7, @babel/parser@npm:^7.24.8, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": - version: 7.24.8 - resolution: "@babel/parser@npm:7.24.8" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.24.8, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.6.0, @babel/parser@npm:^7.9.6": + version: 7.25.0 + resolution: "@babel/parser@npm:7.25.0" bin: parser: ./bin/babel-parser.js - checksum: 10c0/ce69671de8fa6f649abf849be262707ac700b573b8b1ce1893c66cc6cd76aeb1294a19e8c290b0eadeb2f47d3f413a2e57a281804ffbe76bfb9fa50194cf3c52 + checksum: 10c0/4aecf13829fa6f4a66835429bd235458544d9cd14374b17c19bc7726f472727ca33f500e51e1298ddc72db93bdd77fcaa9ddc095200b0b792173069e6cf9742e languageName: node linkType: hard @@ -414,51 +376,48 @@ __metadata: linkType: hard "@babel/runtime@npm:^7.23.9": - version: 7.24.8 - resolution: "@babel/runtime@npm:7.24.8" + version: 7.25.0 + resolution: "@babel/runtime@npm:7.25.0" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 10c0/f24b30af6b3ecae19165b3b032f9bc37b2d1769677bd63b69a6f81061967cfc847aa822518402ea6616b1d301d7eb46986b99c9f69cdb5880834fca2e6b34881 + checksum: 10c0/bd3faf246170826cef2071a94d7b47b49d532351360ecd17722d03f6713fd93a3eb3dbd9518faa778d5e8ccad7392a7a604e56bd37aaad3f3aa68d619ccd983d languageName: node linkType: hard -"@babel/template@npm:^7.24.7, @babel/template@npm:^7.3.3": - version: 7.24.7 - resolution: "@babel/template@npm:7.24.7" +"@babel/template@npm:^7.24.7, @babel/template@npm:^7.25.0, @babel/template@npm:^7.3.3": + version: 7.25.0 + resolution: "@babel/template@npm:7.25.0" dependencies: "@babel/code-frame": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10c0/95b0b3ee80fcef685b7f4426f5713a855ea2cd5ac4da829b213f8fb5afe48a2a14683c2ea04d446dbc7f711c33c5cd4a965ef34dcbe5bc387c9e966b67877ae3 + "@babel/parser": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" + checksum: 10c0/4e31afd873215744c016e02b04f43b9fa23205d6d0766fb2e93eb4091c60c1b88897936adb895fb04e3c23de98dfdcbe31bc98daaa1a4e0133f78bb948e1209b languageName: node linkType: hard -"@babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8": - version: 7.24.8 - resolution: "@babel/traverse@npm:7.24.8" +"@babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0": + version: 7.25.0 + resolution: "@babel/traverse@npm:7.25.0" dependencies: "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.24.8" - "@babel/helper-environment-visitor": "npm:^7.24.7" - "@babel/helper-function-name": "npm:^7.24.7" - "@babel/helper-hoist-variables": "npm:^7.24.7" - "@babel/helper-split-export-declaration": "npm:^7.24.7" - "@babel/parser": "npm:^7.24.8" - "@babel/types": "npm:^7.24.8" + "@babel/generator": "npm:^7.25.0" + "@babel/parser": "npm:^7.25.0" + "@babel/template": "npm:^7.25.0" + "@babel/types": "npm:^7.25.0" debug: "npm:^4.3.1" globals: "npm:^11.1.0" - checksum: 10c0/67a5cc35824455cdb54fb9e196a44b3186283e29018a9c2331f51763921e18e891b3c60c283615a27540ec8eb4c8b89f41c237b91f732a7aa518b2eb7a0d434d + checksum: 10c0/a958365fd8527b6572d63c1f50a646dda3d8b8551630588d44ae89f6eaa56b81417f2108006549f757a483b3c69f872f607ff1ed402c6ba08c389b4429ff13db languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.8, @babel/types@npm:^7.24.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.6.1, @babel/types@npm:^7.8.3, @babel/types@npm:^7.9.6": - version: 7.24.9 - resolution: "@babel/types@npm:7.24.9" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.24.9, @babel/types@npm:^7.25.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.6.1, @babel/types@npm:^7.8.3, @babel/types@npm:^7.9.6": + version: 7.25.0 + resolution: "@babel/types@npm:7.25.0" dependencies: "@babel/helper-string-parser": "npm:^7.24.8" "@babel/helper-validator-identifier": "npm:^7.24.7" to-fast-properties: "npm:^2.0.0" - checksum: 10c0/4970b3481cab39c5c3fdb7c28c834df5c7049f3c7f43baeafe121bb05270ebf0da7c65b097abf314877f213baa591109c82204f30d66cdd46c22ece4a2f32415 + checksum: 10c0/3b2087d72442d53944b5365c7082f120e5040b0333d4a82406187c19056261ae2a35e087f8408348baadf1dcd156dc74573ec151272191b4a22b564297473da1 languageName: node linkType: hard @@ -1293,16 +1252,6 @@ __metadata: languageName: node linkType: hard -"@nestjs/passport@npm:^10.0.2": - 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.3.10 resolution: "@nestjs/platform-express@npm:10.3.10" @@ -1347,7 +1296,7 @@ __metadata: languageName: node linkType: hard -"@nestjs/swagger@npm:^7.1.15": +"@nestjs/swagger@npm:^7.4.0": version: 7.4.0 resolution: "@nestjs/swagger@npm:7.4.0" dependencies: @@ -1371,7 +1320,7 @@ __metadata: optional: true class-validator: optional: true - checksum: 10c0/6dca99984bea2303353cd890b622b037e8cb4129c38047883936c619073e0a94ccb13ca98ab68d3ad5d0bc96564f94b9e83809a7489f36e2cae4c553c385d779 + checksum: 10c0/ba7d72155662294fb9af6aafdec45403c20d748b017fa2eac34f152864e4d5283aae903daa0502553e4340a39f6280c6fdcae1dcbc82f36f14e2a22f6fe8cccf languageName: node linkType: hard @@ -1737,7 +1686,7 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.17": +"@types/express@npm:^4.17.17": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -1816,7 +1765,7 @@ __metadata: languageName: node linkType: hard -"@types/jsonwebtoken@npm:^9.0.2": +"@types/jsonwebtoken@npm:^9, @types/jsonwebtoken@npm:^9.0.2": version: 9.0.6 resolution: "@types/jsonwebtoken@npm:9.0.6" dependencies: @@ -1860,12 +1809,21 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^20.10.3, @types/node@npm:^20.3.1": - version: 20.14.12 - resolution: "@types/node@npm:20.14.12" +"@types/node@npm:*, @types/node@npm:>=13.7.0": + version: 22.0.0 + resolution: "@types/node@npm:22.0.0" + dependencies: + undici-types: "npm:~6.11.1" + checksum: 10c0/af26a8ec7266c857b0ced75dc3a93c6b65280d1fa40d1b4488c814d30831c5c752489c99ecb5698daec1376145b1a9ddd08350882dc2e07769917a5f22a460bc + languageName: node + linkType: hard + +"@types/node@npm:^20.10.3, @types/node@npm:^20.3.1": + version: 20.14.13 + resolution: "@types/node@npm:20.14.13" dependencies: undici-types: "npm:~5.26.4" - checksum: 10c0/59bc5fa11fdd23fd517f859063118f54a1ab53d3399ef63c926f8902429d7453abc0db22ef4b0a6110026b6ab81b6472fee894e1d235c24b01a0b3e10cfae0bb + checksum: 10c0/10bb3ece675308742301c652ab8c6cb88b1ebddebed22316103c58f94fe7eff131edd5f679e487c19077fadb6b5e6b1ad9a60a2cee2869aa1f20452b9761d570 languageName: node linkType: hard @@ -1878,36 +1836,6 @@ __metadata: languageName: node linkType: hard -"@types/passport-local@npm:^1.0.37": - version: 1.0.38 - resolution: "@types/passport-local@npm:1.0.38" - dependencies: - "@types/express": "npm:*" - "@types/passport": "npm:*" - "@types/passport-strategy": "npm:*" - checksum: 10c0/a8464df03f073a4bb9aef7fa7cc9e76a355f149a1148330da88346d0e9c600f845601e99ed40949a13287eacae0a7ad01cd0eb5ca00d8b81da263b1dfc3aee60 - 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/pug@npm:^2.0.10": version: 2.0.10 resolution: "@types/pug@npm:2.0.10" @@ -1970,13 +1898,14 @@ __metadata: linkType: hard "@types/superagent@npm:*": - version: 8.1.7 - resolution: "@types/superagent@npm:8.1.7" + version: 8.1.8 + resolution: "@types/superagent@npm:8.1.8" dependencies: "@types/cookiejar": "npm:^2.1.5" "@types/methods": "npm:^1.1.4" "@types/node": "npm:*" - checksum: 10c0/4676d539f5feaaea9d39d7409c86ae9e15b92a43c28456aff9d9897e47e9fe5ebd3807600c5310f84fe5ebea30f3fe5e2b3b101a87821a478ca79e3a56fd8c9e + form-data: "npm:^4.0.0" + checksum: 10c0/c5fa8fe48e63445317d2e056c93c373a14cd916ac7b6e5a084f8cdecc70419683c89e3245ad47ff3d1f33406cfdc23117e3877651b184257adcd3063b7037feb languageName: node linkType: hard @@ -3664,14 +3593,14 @@ __metadata: linkType: hard "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.4": - version: 4.3.5 - resolution: "debug@npm:4.3.5" + version: 4.3.6 + resolution: "debug@npm:4.3.6" dependencies: ms: "npm:2.1.2" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/082c375a2bdc4f4469c99f325ff458adad62a3fc2c482d59923c260cb08152f34e2659f72b3767db8bb2f21ca81a60a42d1019605a412132d7b9f59363a005cc + checksum: 10c0/3293416bff072389c101697d4611c402a6bacd1900ac20c0492f61a9cdd6b3b29750fc7f5e299f8058469ef60ff8fb79b86395a30374fbd2490113c1c7112285 languageName: node linkType: hard @@ -4464,13 +4393,13 @@ __metadata: linkType: hard "fast-xml-parser@npm:^4.3.0": - version: 4.4.0 - resolution: "fast-xml-parser@npm:4.4.0" + version: 4.4.1 + resolution: "fast-xml-parser@npm:4.4.1" dependencies: strnum: "npm:^1.0.5" bin: fxparser: src/cli/cli.js - checksum: 10c0/ce32fad713471a40bea67959894168f297a5dd0aba64b89a2abc71a4fec0b1ae1d49c2dd8d8719ca8beeedf477824358c8a486b360b9f3ef12abc2e355d11318 + checksum: 10c0/7f334841fe41bfb0bf5d920904ccad09cefc4b5e61eaf4c225bf1e1bb69ee77ef2147d8942f783ee8249e154d1ca8a858e10bda78a5d78b8bed3f48dcee9bf33 languageName: node linkType: hard @@ -4993,8 +4922,8 @@ __metadata: linkType: hard "google-auth-library@npm:^9.3.0, google-auth-library@npm:^9.6.3": - version: 9.11.0 - resolution: "google-auth-library@npm:9.11.0" + version: 9.12.0 + resolution: "google-auth-library@npm:9.12.0" dependencies: base64-js: "npm:^1.3.0" ecdsa-sig-formatter: "npm:^1.0.11" @@ -5002,7 +4931,7 @@ __metadata: gcp-metadata: "npm:^6.1.0" gtoken: "npm:^7.0.0" jws: "npm:^4.0.0" - checksum: 10c0/0cbaf72d6f4acc891e0fee26864c625b770d6a375a391d147fee0f9fc9e7df331b6915a78260a17ea12da8a72662203e2e4609077fe90ad50a531fc60684cd11 + checksum: 10c0/1fd3bb8c906eb2db94a282b2b8a59d88d5dcb3df6bbea06715f7d8dc3dc077fe66a2934cda94f786c575f367438c3866558b681d27c84b5a4fbe50c07e118cfa languageName: node linkType: hard @@ -6510,7 +6439,7 @@ __metadata: languageName: node linkType: hard -"jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0": +"jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0, jsonwebtoken@npm:^9.0.2": version: 9.0.2 resolution: "jsonwebtoken@npm:9.0.2" dependencies: @@ -6659,20 +6588,19 @@ __metadata: "@nestjs/config": "npm:^3.1.1" "@nestjs/core": "npm:^10.0.0" "@nestjs/jwt": "npm:^10.1.1" - "@nestjs/passport": "npm:^10.0.2" "@nestjs/platform-express": "npm:^10.0.0" "@nestjs/schedule": "npm:^4.0.0" "@nestjs/schematics": "npm:^10.0.0" - "@nestjs/swagger": "npm:^7.1.15" + "@nestjs/swagger": "npm:^7.4.0" "@nestjs/testing": "npm:^10.0.0" "@nestjs/typeorm": "npm:^10.0.0" "@types/bcrypt": "npm:^5.0.1" "@types/express": "npm:^4.17.17" "@types/jest": "npm:^29.5.2" + "@types/jsonwebtoken": "npm:^9" "@types/lodash": "npm:^4" "@types/node": "npm:^20.3.1" "@types/nodemailer": "npm:^6" - "@types/passport-local": "npm:^1.0.37" "@types/supertest": "npm:^2.0.12" axios: "npm:^1.6.0" bcrypt: "npm:^5.1.1" @@ -6683,12 +6611,10 @@ __metadata: firebase-admin: "npm:^12.0.0" htmlparser2: "npm:^9.1.0" jest: "npm:^29.5.0" + jsonwebtoken: "npm:^9.0.2" lodash: "npm:^4.17.21" nestjs-pino: "npm:^4.1.0" nodemailer: "npm:^6.9.14" - passport: "npm:^0.6.0" - passport-jwt: "npm:^4.0.1" - passport-local: "npm:^1.0.0" pg: "npm:^8.11.3" pino: "npm:^9.3.2" pino-http: "npm:^10.2.0" @@ -6761,9 +6687,9 @@ __metadata: linkType: hard "libphonenumber-js@npm:^1.10.53": - version: 1.11.4 - resolution: "libphonenumber-js@npm:1.11.4" - checksum: 10c0/0a606da67b4b465e6e157570ad5e70b92f59197cdc1c505d160422a21a894b55a75c9044b863d0eaf4d96884d3fa3e77268adf55afc2d8f11efae7f7a249e7cc + version: 1.11.5 + resolution: "libphonenumber-js@npm:1.11.5" + checksum: 10c0/c57b4d75d56fc32e611ea2dfdb196f18413caa1d72923250631ed0db915edb958b50f2ecee062241dbce98d9ed7afb6fe744776b6b325746cff0ab44ed69e756 languageName: node linkType: hard @@ -8306,43 +8232,6 @@ __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-local@npm:^1.0.0": - version: 1.0.0 - resolution: "passport-local@npm:1.0.0" - dependencies: - passport-strategy: "npm:1.x.x" - checksum: 10c0/59becb988014921a5d6056470d9373c41db452fcf113323064f39d53baa6f184e72151bf269ca6770511f7f0260e13632dacc7b6afdbf60ebf63e90327e186d4 - 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.6.0": - version: 0.6.0 - resolution: "passport@npm:0.6.0" - dependencies: - passport-strategy: "npm:1.x.x" - pause: "npm:0.0.1" - utils-merge: "npm:^1.0.1" - checksum: 10c0/1d8651a4a1a72b84ea08c498cff9cfc209aebfe18baed4cf93292ded3f8e30a04e30b404fdfce39dfb6aa7247e205f1df43fbfd7bc7c1a67a600884359d46ee6 - languageName: node - linkType: hard - "path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -8409,13 +8298,6 @@ __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 - "peberminta@npm:^0.9.0": version: 0.9.0 resolution: "peberminta@npm:0.9.0" @@ -10542,11 +10424,11 @@ __metadata: linkType: hard "uglify-js@npm:^3.1.4, uglify-js@npm:^3.5.1": - version: 3.19.0 - resolution: "uglify-js@npm:3.19.0" + version: 3.19.1 + resolution: "uglify-js@npm:3.19.1" bin: uglifyjs: bin/uglifyjs - checksum: 10c0/c27d7a4734a59c5e2c08a6efd68bc534d559619f80ad437b1009ed56a7b1a8f6d6cbd5892a15879e0413d724e785b7227487ccca8d3e07261ba92d469c1447d3 + checksum: 10c0/7609ab3f10d54de5ef014770f845c747266a969e9092d2284ca0ba18f10a4488208c1491bd8b52bd4c40cf6687b47a77c495f08e4a625babcdd57f58e34a3976 languageName: node linkType: hard @@ -10566,6 +10448,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.11.1": + version: 6.11.1 + resolution: "undici-types@npm:6.11.1" + checksum: 10c0/d8f5739a8e6c779d72336c82deb49c56d5ac9f9f6e0eb2e8dd4d3f6929ae9db7cde370d2e46516fe6cad04ea53e790c5e16c4c75eed7cd0f9bd31b0763bb2fa3 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0" @@ -10651,7 +10540,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 From 6caf145f3730cb89d0e025fca5aa9ee7c887de6e Mon Sep 17 00:00:00 2001 From: overthestream Date: Mon, 29 Jul 2024 01:51:56 +0900 Subject: [PATCH 07/11] refactor: auth domain --- src/asset/error.json | 19 +++++++++ src/common/decorators/docs/auth.decorator.ts | 20 +++------- .../decorators/docs/category.decorator.ts | 6 +-- src/common/decorators/useValidation.ts | 19 +++++++++ src/common/utils/exception.ts | 2 +- src/domain/auth/auth.controller.ts | 8 +++- src/domain/auth/dtos/changePwdRequest.dto.ts | 11 +++--- src/domain/auth/dtos/loginRequestDto.ts | 25 +++--------- src/domain/auth/dtos/signupRequest.dto.ts | 34 +++++++++++----- src/domain/category/category.controller.ts | 16 ++++---- src/domain/notice/notice.controller.ts | 20 +++++----- .../notification/notification.controller.ts | 39 ++++++++----------- src/domain/scrap/scrap.controller.ts | 37 +++++++----------- src/domain/subscribe/subscribe.controller.ts | 38 +++++++----------- src/domain/users/users.controller.ts | 12 +++--- src/main.ts | 3 +- 16 files changed, 160 insertions(+), 149 deletions(-) create mode 100644 src/common/decorators/useValidation.ts diff --git a/src/asset/error.json b/src/asset/error.json index 34ea501..40ff63b 100644 --- a/src/asset/error.json +++ b/src/asset/error.json @@ -105,6 +105,25 @@ "name": "TODO_INVALID", "message": "할." }, + "NOT_ACCEPTABLE": { + "errorCode": 4060, + "statusCode": 406, + "name": "NOT_ACCEPTABLE", + "message": "요청이 올바르지 않습니다. 비어있거나 잘못된 형식입니다." + }, + "EMAIL_NOT_VALID": { + "errorCode": 4062, + "statusCode": 406, + "name": "EMAIL_NOT_VALID", + "message": "이메일 형식이 올바르지 않습니다. korea.ac.kr 이메일을 입력해주세요." + }, + "PASSWORD_NOT_VALID": { + "errorCode": 4063, + "statusCode": 406, + "name": "PASSWORD_NOT_VALID", + "message": "비밀번호가 올바르지 않습니다. 8자-20자로 특수문자, 영어, 숫자의 조합으로 입력해주세요." + }, + "TOO_MANY_REQUESTS": { "errorCode": 4290, "statusCode": 429, diff --git a/src/common/decorators/docs/auth.decorator.ts b/src/common/decorators/docs/auth.decorator.ts index 36c0316..b775442 100644 --- a/src/common/decorators/docs/auth.decorator.ts +++ b/src/common/decorators/docs/auth.decorator.ts @@ -1,3 +1,4 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; import type { MethodNames } from '@/common/types/method'; import type { AuthController } from '@/domain/auth/auth.controller'; import { LoginRequestDto } from '@/domain/auth/dtos/loginRequestDto'; @@ -9,7 +10,6 @@ import { ApiOkResponse, ApiOperation, } from '@nestjs/swagger'; -import { ApiKudogExceptionResponse } from '@/common/decorators'; type AuthEndpoints = MethodNames; @@ -43,10 +43,6 @@ const AuthDocsMap: Record = { 'EMAIL_VALIDATION_EXPIRED', 'EMAIL_NOT_VALIDATED', 'EMAIL_ALREADY_USED', - 'EMAIL_NOT_IN_KOREA_DOMAIN', - 'PASSWORD_INVALID_FORMAT', - //TODO: ERROR instantize - 'TODO_INVALID', ]), ], refresh: [ @@ -65,8 +61,7 @@ const AuthDocsMap: Record = { description: '토큰 재발급 성공', type: TokenResponseDto, }), - //TODO:TODO: - ApiKudogExceptionResponse(['TODO_REFRESH']), + ApiKudogExceptionResponse(['LOGIN_REQUIRED']), ], logout: [ ApiOperation({ @@ -80,7 +75,7 @@ const AuthDocsMap: Record = { name: 'authorization', required: true, }), - ApiKudogExceptionResponse(['INVALID_ACCESS_TOKEN', 'USER_NOT_FOUND']), + ApiKudogExceptionResponse(['JWT_TOKEN_INVALID']), ApiOkResponse({ description: 'logout 성공', }), @@ -91,7 +86,7 @@ const AuthDocsMap: Record = { description: '회원 탈퇴합니다. authorization header에 Bearer ${accessToken} 을 담아주세요.', }), - ApiKudogExceptionResponse(['INVALID_ACCESS_TOKEN']), + ApiKudogExceptionResponse(['USER_NOT_FOUND']), ApiOkResponse({ description: '회원 탈퇴 성공', }), @@ -105,10 +100,8 @@ const AuthDocsMap: Record = { description: '이메일 전송 성공. 3분 안에 인증 코드를 입력해주세요.', }), ApiKudogExceptionResponse([ - 'USER_NOT_FOUND', + 'EMAIL_NOT_FOUND', 'TOO_MANY_REQUESTS', - 'EMAIL_NOT_IN_KOREA_DOMAIN', - 'TODO_INVALID', 'EMAIL_SEND_FAILED', ]), ], @@ -137,8 +130,7 @@ const AuthDocsMap: Record = { ApiKudogExceptionResponse([ 'USER_NOT_FOUND', 'CODE_NOT_VALIDATED', - 'CODE_EXPIRED', - 'TODO_INVALID', + 'CODE_VALIDATION_EXPIRED', ]), ], }; diff --git a/src/common/decorators/docs/category.decorator.ts b/src/common/decorators/docs/category.decorator.ts index 24afbc1..b00df7f 100644 --- a/src/common/decorators/docs/category.decorator.ts +++ b/src/common/decorators/docs/category.decorator.ts @@ -17,7 +17,7 @@ const CategoryDocsMap: Record = { description: '학부 리스트', type: [ProviderListResponseDto], }), - ApiKudogExceptionResponse(['ACCESS_TOKEN_EXPIRED']), + ApiKudogExceptionResponse([]), ], getCategories: [ ApiOperation({ @@ -36,7 +36,7 @@ const CategoryDocsMap: Record = { description: '스크랩학부 소속 카테고리들', type: [CategoryListResponseDto], }), - ApiKudogExceptionResponse(['ACCESS_TOKEN_EXPIRED', 'TODO_INVALID']), + ApiKudogExceptionResponse([]), ], getBookmarkedProviders: [ ApiOperation({ @@ -48,7 +48,7 @@ const CategoryDocsMap: Record = { description: '사용자의 즐겨찾는 학과 목록', type: [ProviderListResponseDto], }), - ApiKudogExceptionResponse(['ACCESS_TOKEN_EXPIRED']), + ApiKudogExceptionResponse([]), ], }; diff --git a/src/common/decorators/useValidation.ts b/src/common/decorators/useValidation.ts new file mode 100644 index 0000000..249ccb6 --- /dev/null +++ b/src/common/decorators/useValidation.ts @@ -0,0 +1,19 @@ +import { UsePipes, ValidationPipe, applyDecorators } from '@nestjs/common'; +import { type ExceptionNames, throwKudogException } from '../utils/exception'; +import { ApiKudogExceptionResponse } from './apiKudogExceptionResponse.decorator'; + +export function UseValidation(exceptions: ExceptionNames[]): MethodDecorator { + return applyDecorators( + UsePipes( + new ValidationPipe({ + transform: true, + exceptionFactory(errs) { + + const err = Object.values(errs[0].contexts)[0].exception; + throwKudogException(err); + }, + }), + ), + ApiKudogExceptionResponse(exceptions), + ); +} diff --git a/src/common/utils/exception.ts b/src/common/utils/exception.ts index 1974ae4..a07676d 100644 --- a/src/common/utils/exception.ts +++ b/src/common/utils/exception.ts @@ -26,4 +26,4 @@ export class HttpException extends Error { export function throwKudogException(name: ExceptionNames): never { throw new HttpException(name); -} +} \ No newline at end of file diff --git a/src/domain/auth/auth.controller.ts b/src/domain/auth/auth.controller.ts index d14b2f7..4964dca 100644 --- a/src/domain/auth/auth.controller.ts +++ b/src/domain/auth/auth.controller.ts @@ -1,7 +1,8 @@ import { InjectToken, InjectUser, NamedController } from '@/common/decorators'; import { AuthDocs } from '@/common/decorators/docs'; +import { UseValidation } from '@/common/decorators/useValidation'; import { JwtPayload } from '@/common/types/auth'; -import { Body, Delete, Header, Post, Put } from '@nestjs/common'; +import { Body, Delete, Post, Put } from '@nestjs/common'; import { AuthService } from './auth.service'; import { ChangePasswordDto, @@ -18,11 +19,13 @@ import { UseJwtGuard } from './guards/jwt.guard'; export class AuthController { constructor(private readonly authService: AuthService) {} + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID', 'PASSWORD_NOT_VALID']) @Post('/login') async login(@Body() body: LoginRequestDto): Promise { return this.authService.login(body); } + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID', 'PASSWORD_NOT_VALID']) @Post('/signup') async signup(@Body() body: SignupRequestDto): Promise { return this.authService.signup(body); @@ -49,6 +52,7 @@ export class AuthController { return this.authService.deleteUser(user.id); } + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID']) @Post('/change-password/request') async changePwdRequest( @Body() body: ChangePasswordRequestDto, @@ -56,6 +60,7 @@ export class AuthController { return this.authService.changePwdRequest(body); } + @UseValidation(['NOT_ACCEPTABLE']) @Post('/change-password/verify') async verifyChangePwdCode( @Body() body: VerifyChangePasswordRequestDto, @@ -63,6 +68,7 @@ export class AuthController { return this.authService.verifyChangePwdCode(body); } + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID', 'PASSWORD_NOT_VALID']) @Put('/change-password') async changePassword(@Body() body: ChangePasswordDto): Promise { return this.authService.changePassword(body); diff --git a/src/domain/auth/dtos/changePwdRequest.dto.ts b/src/domain/auth/dtos/changePwdRequest.dto.ts index ad4c29c..7b65324 100644 --- a/src/domain/auth/dtos/changePwdRequest.dto.ts +++ b/src/domain/auth/dtos/changePwdRequest.dto.ts @@ -1,16 +1,17 @@ import { ApiProperty, PickType } from '@nestjs/swagger'; +import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; import { SignupRequestDto } from './signupRequest.dto'; -import { IsString, MaxLength, MinLength } from 'class-validator'; export class ChangePasswordRequestDto extends PickType(SignupRequestDto, [ 'email', ] as const) {} export class VerifyChangePasswordRequestDto { - @IsString({ message: 'code는 숫자로 이루어진 문자열이어야 합니다.' }) - @MaxLength(6, { message: '인증코드는 6자리여야 합니다.' }) - @MinLength(6, { message: '인증코드는 6자리여야 합니다.' }) - @ApiProperty({ example: '102345', description: '인증코드' }) + @MaxLength(6, { context: { exception: 'NOT_ACCEPTABLE' } }) + @MinLength(6, { context: { exception: 'NOT_ACCEPTABLE' } }) + @IsString({ context: { exception: 'NOT_ACCEPTABLE' } }) + @IsNotEmpty() + @ApiProperty({ example: '102345', description: '인증코드, 6자리 숫자스트링' }) code: string; } diff --git a/src/domain/auth/dtos/loginRequestDto.ts b/src/domain/auth/dtos/loginRequestDto.ts index 51d95f8..2a974d7 100644 --- a/src/domain/auth/dtos/loginRequestDto.ts +++ b/src/domain/auth/dtos/loginRequestDto.ts @@ -1,20 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { PickType } from '@nestjs/swagger'; +import { SignupRequestDto } from './signupRequest.dto'; -export class LoginRequestDto { - @IsEmail( - { host_whitelist: ['korea.ac.kr'] }, - { message: '유효하지 않은 이메일 주소입니다.' }, - ) - @IsNotEmpty({ message: '이메일 주소는 필수 항목입니다.' }) - @ApiProperty({ - example: 'devkor.apply@gmail.com', - description: 'portal email을 이용하여 로그인합니다.', - }) - email: string; - @IsString({ message: '비밀번호는 문자열로 이루어져야 합니다.' }) - @IsNotEmpty({ message: '비밀번호는 필수 항목입니다.' }) - @MinLength(8, { message: '비밀번호는 최소 8자리 이상이어야 합니다.' }) - @ApiProperty({ example: 'password1' }) - password: string; -} +export class LoginRequestDto extends PickType(SignupRequestDto, [ + 'password', + 'email', +] as const) {} diff --git a/src/domain/auth/dtos/signupRequest.dto.ts b/src/domain/auth/dtos/signupRequest.dto.ts index 8591acf..f671c02 100644 --- a/src/domain/auth/dtos/signupRequest.dto.ts +++ b/src/domain/auth/dtos/signupRequest.dto.ts @@ -1,23 +1,37 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator'; export class SignupRequestDto { - @IsString({ message: '이름은 문자열로 이루어져야 합니다.' }) - @IsNotEmpty({ message: '이름은 필수 항목입니다.' }) + @IsString({ + context: { exception: 'NOT_ACCEPTABLE' }, + }) + @IsNotEmpty({ context: { exception: 'NOT_ACCEPTABLE' } }) @ApiProperty({ example: '홍길동' }) name: string; @IsEmail( { host_whitelist: ['korea.ac.kr'] }, - { message: '유효하지 않은 이메일 주소입니다.' }, + { context: { exception: 'EMAIL_NOT_VALID' } }, ) - @IsNotEmpty({ message: '이메일 주소는 필수 항목입니다.' }) - @ApiProperty({ example: 'devkor.appply@gmail.com' }) + @IsNotEmpty({ + context: { exception: 'NOT_ACCEPTABLE' }, + }) + @ApiProperty({ + example: 'devkor.appply@korea.ac.kr', + description: 'korea.ac.kr 이메일만 입력받습니다.', + }) email: string; - @IsString({ message: '비밀번호는 문자열로 이루어져야 합니다.' }) - @IsNotEmpty({ message: '비밀번호는 필수 항목입니다.' }) - @MinLength(8, { message: '비밀번호는 최소 8자리 이상이어야 합니다.' }) - @ApiProperty({ example: 'password1' }) + @Matches(/^(?=.*[a-zA-Z0-9])(?=.*[~!@#$%^&*])[a-zA-Z0-9~!@#$%^&*]{8,20}$/, { + context: { exception: 'PASSWORD_NOT_VALID' }, + }) + @IsString({ context: { exception: 'NOT_ACCEPTABLE' } }) + @IsNotEmpty({ + context: { exception: 'NOT_ACCEPTABLE' }, + }) + @ApiProperty({ + example: 'password1~', + description: '8~20자리 영어 + 숫자 + 특수문자 비밀번호', + }) password: string; } diff --git a/src/domain/category/category.controller.ts b/src/domain/category/category.controller.ts index 11da3d6..9a61782 100644 --- a/src/domain/category/category.controller.ts +++ b/src/domain/category/category.controller.ts @@ -1,9 +1,9 @@ -import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { InjectUser, NamedController } from '@/common/decorators'; import { CategoryDocs } from '@/common/decorators/docs/category.decorator'; import { JwtPayload } from '@/common/types/auth'; +import { IntValidationPipe } from '@/pipes/intValidation.pipe'; import { Get, Param } from '@nestjs/common'; -import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { CategoryService } from './category.service'; import { ProviderListResponseDto } from './dtos/ProviderListResponse.dto'; import { CategoryListResponseDto } from './dtos/categoryListResponse.dto'; @@ -13,24 +13,24 @@ import { CategoryListResponseDto } from './dtos/categoryListResponse.dto'; export class CategoryController { constructor(private readonly categoryService: CategoryService) {} - @JwtAccessGuard() + @UseJwtGuard() @Get('/providers') async getProviders(): Promise { return this.categoryService.getProviders(); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/by-providers/:id') async getCategories( - @Param('id', IntValidationPipe) id: number, + @Param('id', new IntValidationPipe()) id: number, ): Promise { return this.categoryService.getCategories(id); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/providers/bookmarks') async getBookmarkedProviders( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.categoryService.getBookmarkedProviders(user.id); } diff --git a/src/domain/notice/notice.controller.ts b/src/domain/notice/notice.controller.ts index 2d88d00..2ca7b32 100644 --- a/src/domain/notice/notice.controller.ts +++ b/src/domain/notice/notice.controller.ts @@ -1,4 +1,4 @@ -import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { InjectUser, NamedController } from '@/common/decorators'; import { UsePagination } from '@/common/decorators'; import { NoticeDocs } from '@/common/decorators/docs'; import { PageQuery } from '@/common/dtos/pageQuery'; @@ -6,7 +6,7 @@ import { PageResponse } from '@/common/dtos/pageResponse'; import { JwtPayload } from '@/common/types/auth'; import { Body, Get, Param, Post, Put, Query } from '@nestjs/common'; import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { AddRequestRequestDto } from './dtos/AddRequestRequest.dto'; import { NoticeFilterRequestDto } from './dtos/NoticeFilterRequest.dto'; import { NoticeInfoResponseDto } from './dtos/NoticeInfoResponse.dto'; @@ -18,10 +18,10 @@ import { NoticeService } from './notice.service'; export class NoticeController { constructor(private readonly noticeService: NoticeService) {} - @JwtAccessGuard() + @UseJwtGuard() @Get('/list') async getNoticeList( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, @Query() filter: NoticeFilterRequestDto, @Query('keyword') keyword?: string, @@ -34,29 +34,29 @@ export class NoticeController { ); } - @JwtAccessGuard() + @UseJwtGuard() @Put('/:noticeId/scrap/:scrapBoxId') async scrapNotice( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Param('noticeId', IntValidationPipe) noticeId: number, @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, ): Promise { return this.noticeService.scrapNotice(user.id, noticeId, scrapBoxId); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/info/:id') async getNoticeInfoById( @Param('id', IntValidationPipe) id: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.noticeService.getNoticeInfoById(id, user.id); } - @JwtAccessGuard() + @UseJwtGuard() @Post('/add-request') async addNoticeRequest( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: AddRequestRequestDto, ): Promise { return this.noticeService.addNoticeRequest(body); diff --git a/src/domain/notification/notification.controller.ts b/src/domain/notification/notification.controller.ts index 6e4a174..339c00a 100644 --- a/src/domain/notification/notification.controller.ts +++ b/src/domain/notification/notification.controller.ts @@ -1,11 +1,11 @@ -import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { InjectUser, NamedController } from '@/common/decorators'; import { UsePagination } from '@/common/decorators'; import { NotificationDocs } from '@/common/decorators/docs'; import { PageQuery } from '@/common/dtos/pageQuery'; import { PageResponse } from '@/common/dtos/pageResponse'; import { JwtPayload } from '@/common/types/auth'; import { Body, Delete, Get, Post } from '@nestjs/common'; -import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { NotificationInfoResponseDto } from './dtos/noticiationInfoResponse.dto'; import { TokenRequestDto } from './dtos/tokenRequest.dto'; import { NotificationService } from './notification.service'; @@ -15,61 +15,54 @@ import { NotificationService } from './notification.service'; export class NotificationController { constructor(private readonly notificationService: NotificationService) {} - @JwtAccessGuard() + @UseJwtGuard() @Get('') async getNotifications( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { return this.notificationService.getNotifications(user.id, pageQuery); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/new') async getNewNotifications( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { - return this.notificationService.getNewNotifications( - user.id, - pageQuery, - ); + return this.notificationService.getNewNotifications(user.id, pageQuery); } - @JwtAccessGuard() + @UseJwtGuard() @Post('/token') async registerToken( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: TokenRequestDto, ): Promise { return this.notificationService.registerToken(user.id, body.token); } - @JwtAccessGuard() + @UseJwtGuard() @Delete('/token') async deleteToken( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: TokenRequestDto, ): Promise { return this.notificationService.deleteToken(user.id, body.token); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/status') async getTokenStatus( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: TokenRequestDto, ): Promise { return this.notificationService.getTokenStatus(user.id, body.token); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/test') - async sendNotification(@InjectAccessUser() user: JwtPayload): Promise { - return this.notificationService.sendNotification( - [user.id], - 'test', - 'test', - ); + async sendNotification(@InjectUser() user: JwtPayload): Promise { + return this.notificationService.sendNotification([user.id], 'test', 'test'); } } diff --git a/src/domain/scrap/scrap.controller.ts b/src/domain/scrap/scrap.controller.ts index e88caf6..e15008d 100644 --- a/src/domain/scrap/scrap.controller.ts +++ b/src/domain/scrap/scrap.controller.ts @@ -1,24 +1,13 @@ -import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { InjectUser, NamedController } from '@/common/decorators'; import { UsePagination } from '@/common/decorators'; import { ScrapDocs } from '@/common/decorators/docs'; import { PageQuery } from '@/common/dtos/pageQuery'; import { PageResponse } from '@/common/dtos/pageResponse'; import { JwtPayload } from '@/common/types/auth'; -import { - Body, - Controller, - Delete, - Get, - Param, - Post, - Put, - UseGuards, -} from '@nestjs/common'; -import { AuthGuard } from '@nestjs/passport'; -import { ApiTags } from '@nestjs/swagger'; +import { Body, Delete, Get, Param, Post, Put } from '@nestjs/common'; import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { ScrapBoxRequestDto } from './dtos/scrapBoxRequest.dto'; import { ScrapBoxResponseDto } from './dtos/scrapBoxResponse.dto'; import { ScrapBoxResponseWithNotices } from './dtos/scrapBoxResponseWithNotices.dto'; @@ -29,48 +18,48 @@ import { ScrapService } from './scrap.service'; export class ScrapController { constructor(private readonly scrapService: ScrapService) {} - @JwtAccessGuard() + @UseJwtGuard() @Post('/box') async createScrapBox( @Body() body: ScrapBoxRequestDto, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.scrapService.createScrapBox(user.id, body); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/box/:scrapBoxId') async getScrapBoxInfo( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.scrapService.getScrapBoxInfo(user.id, scrapBoxId); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/box') async getScrapBoxes( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { return this.scrapService.getScrapBoxes(user.id, pageQuery); } - @JwtAccessGuard() + @UseJwtGuard() @Put('/box/:scrapBoxId') async updateScrapBox( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: ScrapBoxRequestDto, ): Promise { return this.scrapService.updateScrapBox(scrapBoxId, user.id, body); } - @JwtAccessGuard() + @UseJwtGuard() @Delete('/box/:scrapBoxId') async deleteScrapBox( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.scrapService.deleteScrapBox(scrapBoxId, user.id); } diff --git a/src/domain/subscribe/subscribe.controller.ts b/src/domain/subscribe/subscribe.controller.ts index 082822c..7a60c86 100644 --- a/src/domain/subscribe/subscribe.controller.ts +++ b/src/domain/subscribe/subscribe.controller.ts @@ -1,20 +1,12 @@ -import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { InjectUser, NamedController } from '@/common/decorators'; import { UsePagination } from '@/common/decorators'; import { SubscribeDocs } from '@/common/decorators/docs'; import { PageQuery } from '@/common/dtos/pageQuery'; import { PageResponse } from '@/common/dtos/pageResponse'; import { JwtPayload } from '@/common/types/auth'; -import { - Body, - Delete, - Get, - Param, - Post, - Put, - Query, -} from '@nestjs/common'; +import { Body, Delete, Get, Param, Post, Put, Query } from '@nestjs/common'; import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { NoticeListResponseDto } from '../notice/dtos/NoticeListResponse.dto'; import { SubscribeBoxRequestDto } from './dtos/subscribeBoxRequest.dto'; import { SubscribeBoxResponseDto } from './dtos/subscribeBoxResponse.dto'; @@ -26,20 +18,20 @@ import { SubscribeService } from './subscribe.service'; export class SubscribeController { constructor(private readonly subscribeService: SubscribeService) {} - @JwtAccessGuard() + @UseJwtGuard() @Post('/box') async createSubscribeBox( @Body() body: SubscribeBoxRequestDto, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.subscribeService.createSubscribeBox(user.id, body); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/box/:subscribeBoxId') async getSubscribeInfo( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Query('date') date: string, ): Promise { return this.subscribeService.getSubscribeBoxInfo( @@ -49,20 +41,20 @@ export class SubscribeController { ); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/box') async getSubscribeBoxes( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { return this.subscribeService.getSubscribeBoxes(user.id, pageQuery); } - @JwtAccessGuard() + @UseJwtGuard() @Put('/box/:subscribeBoxId') async updateSubscribeBox( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: SubscribeBoxRequestDto, ): Promise { return this.subscribeService.updateSubscribeBox( @@ -72,20 +64,20 @@ export class SubscribeController { ); } - @JwtAccessGuard() + @UseJwtGuard() @Delete('/box/:subscribeBoxId') async deleteSubscribeBox( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.subscribeService.deleteSubscribeBox(subscribeBoxId, user.id); } - @JwtAccessGuard() + @UseJwtGuard() @Get('/box/:subscribeBoxId/notices') async getNoticesByBoxWithDate( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Query('date') date: string, ): Promise { return this.subscribeService.getNoticesByBoxWithDate( diff --git a/src/domain/users/users.controller.ts b/src/domain/users/users.controller.ts index 340b43c..cd80f7d 100644 --- a/src/domain/users/users.controller.ts +++ b/src/domain/users/users.controller.ts @@ -1,8 +1,8 @@ -import { InjectAccessUser, NamedController } from '@/common/decorators'; +import { InjectUser, NamedController } from '@/common/decorators'; import { UserDocs } from '@/common/decorators/docs'; import { JwtPayload } from '@/common/types/auth'; import { Body, Get, Put } from '@nestjs/common'; -import { JwtAccessGuard } from '../auth/passport/accessToken.strategy'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { ModifyInfoRequestDto, UserInfoResponseDto } from './dtos/userInfo.dto'; import { UsersService } from './users.service'; @@ -11,18 +11,18 @@ import { UsersService } from './users.service'; export class UsersController { constructor(private readonly userService: UsersService) {} - @JwtAccessGuard() + @UseJwtGuard() @Get('/info') async getUserInfo( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.userService.getUserInfo(user.id); } - @JwtAccessGuard() + @UseJwtGuard() @Put('/info') async modifyUserInfo( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: ModifyInfoRequestDto, ): Promise { return this.userService.modifyUserInfo(user.id, body); diff --git a/src/main.ts b/src/main.ts index 3f0fdbb..1f65f82 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { ExceptionFilter, ValidationPipe } from '@nestjs/common'; +import { ExceptionFilter } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module'; @@ -37,7 +37,6 @@ async function bootstrap() { const logger = app.get(Logger); app.useLogger(logger); app.useGlobalFilters(...filters); - app.useGlobalPipes(new ValidationPipe({ transform: true })); await app.listen(3050); } From 6f1c4e0f83564a0d841d0dfebe02de7d85f6a273 Mon Sep 17 00:00:00 2001 From: overthestream Date: Mon, 29 Jul 2024 02:13:45 +0900 Subject: [PATCH 08/11] format code --- src/common/decorators/docs/notice.decorator.ts | 6 +++--- src/common/decorators/usePagination.decorator.ts | 7 ++++++- src/common/decorators/useValidation.ts | 1 - src/common/types/method.ts | 1 - src/common/utils/exception.ts | 2 +- src/domain/category/category.module.ts | 2 +- src/domain/category/dtos/ProviderListResponse.dto.ts | 2 +- src/domain/mail/mail.module.ts | 6 +++--- src/domain/mail/mail.service.ts | 6 +++--- src/domain/scrap/dtos/scrapBoxResponse.dto.ts | 2 +- src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto.ts | 4 ++-- src/domain/scrap/scrap.module.ts | 4 ++-- .../subscribe/dtos/subscribeBoxResponseWithNotices.dto.ts | 2 +- src/domain/subscribe/subscribe.module.ts | 4 ++-- src/domain/users/users.service.ts | 2 +- src/entities/categoryPerSubscribes.entity.ts | 2 +- src/entities/notice.entity.ts | 2 +- src/entities/notificationToken.entity.ts | 4 ++-- src/entities/provider.entity.ts | 2 +- src/entities/providerBookmark.entity.ts | 2 +- src/entities/scrap.entity.ts | 2 +- src/entities/scrapBox.entity.ts | 2 +- src/entities/subscribeBox.entity.ts | 2 +- src/middlewares/auth.middleware.spec.ts | 1 - src/pipes/intValidation.pipe.ts | 8 ++++---- src/pipes/stringValidation.pipe.ts | 4 ++-- 26 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/common/decorators/docs/notice.decorator.ts b/src/common/decorators/docs/notice.decorator.ts index cc02a2d..2b56886 100644 --- a/src/common/decorators/docs/notice.decorator.ts +++ b/src/common/decorators/docs/notice.decorator.ts @@ -1,4 +1,7 @@ import type { MethodNames } from '@/common/types/method'; +import { AddRequestRequestDto } from '@/domain/notice/dtos/AddRequestRequest.dto'; +import { NoticeInfoResponseDto } from '@/domain/notice/dtos/NoticeInfoResponse.dto'; +import { NoticeListResponseDto } from '@/domain/notice/dtos/NoticeListResponse.dto'; import type { NoticeController } from '@/domain/notice/notice.controller'; import { ApiBody, @@ -8,9 +11,6 @@ import { ApiParam, ApiQuery, } from '@nestjs/swagger'; -import { AddRequestRequestDto } from '@/domain/notice/dtos/AddRequestRequest.dto'; -import { NoticeInfoResponseDto } from '@/domain/notice/dtos/NoticeInfoResponse.dto'; -import { NoticeListResponseDto } from '@/domain/notice/dtos/NoticeListResponse.dto'; import { ApiPagination } from './common.decorator'; type NoticeEndpoints = MethodNames; diff --git a/src/common/decorators/usePagination.decorator.ts b/src/common/decorators/usePagination.decorator.ts index 7cf7024..4c95d7d 100644 --- a/src/common/decorators/usePagination.decorator.ts +++ b/src/common/decorators/usePagination.decorator.ts @@ -17,7 +17,12 @@ export const UsePagination = createParamDecorator( const page = Number.parseInt(request.query.page, 10); const pageSize = Number.parseInt(request.query.pageSize, 10); - if (isNaN(page) || isNaN(pageSize) || page < 1 || pageSize < 1) + if ( + Number.isNaN(page) || + Number.isNaN(pageSize) || + page < 1 || + pageSize < 1 + ) throw new NotAcceptableException('Invalid page query'); return { diff --git a/src/common/decorators/useValidation.ts b/src/common/decorators/useValidation.ts index 249ccb6..e487597 100644 --- a/src/common/decorators/useValidation.ts +++ b/src/common/decorators/useValidation.ts @@ -8,7 +8,6 @@ export function UseValidation(exceptions: ExceptionNames[]): MethodDecorator { new ValidationPipe({ transform: true, exceptionFactory(errs) { - const err = Object.values(errs[0].contexts)[0].exception; throwKudogException(err); }, diff --git a/src/common/types/method.ts b/src/common/types/method.ts index 435e467..40bcf3d 100644 --- a/src/common/types/method.ts +++ b/src/common/types/method.ts @@ -1,4 +1,3 @@ export type MethodNames = { [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; }[keyof T]; - diff --git a/src/common/utils/exception.ts b/src/common/utils/exception.ts index a07676d..1974ae4 100644 --- a/src/common/utils/exception.ts +++ b/src/common/utils/exception.ts @@ -26,4 +26,4 @@ export class HttpException extends Error { export function throwKudogException(name: ExceptionNames): never { throw new HttpException(name); -} \ No newline at end of file +} diff --git a/src/domain/category/category.module.ts b/src/domain/category/category.module.ts index 763c2d7..e61b84f 100644 --- a/src/domain/category/category.module.ts +++ b/src/domain/category/category.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CategoryEntity, ProviderBookmark, ProviderEntity } from 'src/entities'; -import { CategoryService } from './category.service'; import { CategoryController } from './category.controller'; +import { CategoryService } from './category.service'; @Module({ imports: [ diff --git a/src/domain/category/dtos/ProviderListResponse.dto.ts b/src/domain/category/dtos/ProviderListResponse.dto.ts index 34e85f1..055b86c 100644 --- a/src/domain/category/dtos/ProviderListResponse.dto.ts +++ b/src/domain/category/dtos/ProviderListResponse.dto.ts @@ -1,6 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; import { ProviderEntity } from 'src/entities'; import { CategoryListResponseDto } from './categoryListResponse.dto'; -import { ApiProperty } from '@nestjs/swagger'; export class ProviderListResponseDto { @ApiProperty({ diff --git a/src/domain/mail/mail.module.ts b/src/domain/mail/mail.module.ts index b518193..fbdff74 100644 --- a/src/domain/mail/mail.module.ts +++ b/src/domain/mail/mail.module.ts @@ -1,14 +1,14 @@ +import { MailerModule } from '@nestjs-modules/mailer'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { MailController } from './mail.controller'; -import { MailService } from './mail.service'; -import { MailerModule } from '@nestjs-modules/mailer'; import { EmailAuthenticationEntity, KudogUser, Notice, SubscribeBoxEntity, } from 'src/entities'; +import { MailController } from './mail.controller'; +import { MailService } from './mail.service'; @Module({ imports: [ diff --git a/src/domain/mail/mail.service.ts b/src/domain/mail/mail.service.ts index cc7be5e..6d0e316 100644 --- a/src/domain/mail/mail.service.ts +++ b/src/domain/mail/mail.service.ts @@ -1,3 +1,5 @@ +import { getHHMMdate, yesterdayTimeStamp } from '@/common/utils/date'; +import { MailerService } from '@nestjs-modules/mailer'; import { BadRequestException, ConflictException, @@ -5,6 +7,7 @@ import { NotFoundException, RequestTimeoutException, } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; import { EmailAuthenticationEntity, @@ -13,9 +16,6 @@ import { SubscribeBoxEntity, } from 'src/entities'; import { Between, In, Repository } from 'typeorm'; -import { MailerService } from '@nestjs-modules/mailer'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import { getHHMMdate, yesterdayTimeStamp } from '@/common/utils/date'; @Injectable() export class MailService { diff --git a/src/domain/scrap/dtos/scrapBoxResponse.dto.ts b/src/domain/scrap/dtos/scrapBoxResponse.dto.ts index e6db398..ce8fa69 100644 --- a/src/domain/scrap/dtos/scrapBoxResponse.dto.ts +++ b/src/domain/scrap/dtos/scrapBoxResponse.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { ScrapBoxRequestDto } from './scrapBoxRequest.dto'; import { ScrapBoxEntity } from 'src/entities'; +import { ScrapBoxRequestDto } from './scrapBoxRequest.dto'; export class ScrapBoxResponseDto extends ScrapBoxRequestDto { @ApiProperty({ diff --git a/src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto.ts b/src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto.ts index 39795f3..fe1da33 100644 --- a/src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto.ts +++ b/src/domain/scrap/dtos/scrapBoxResponseWithNotices.dto.ts @@ -1,7 +1,7 @@ -import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; -import { ScrapBoxResponseDto } from './scrapBoxResponse.dto'; import { ApiProperty } from '@nestjs/swagger'; +import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; import { ScrapBoxEntity } from 'src/entities'; +import { ScrapBoxResponseDto } from './scrapBoxResponse.dto'; export class ScrapBoxResponseWithNotices extends ScrapBoxResponseDto { @ApiProperty({ diff --git a/src/domain/scrap/scrap.module.ts b/src/domain/scrap/scrap.module.ts index 60de063..cd601dd 100644 --- a/src/domain/scrap/scrap.module.ts +++ b/src/domain/scrap/scrap.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; -import { ScrapService } from './scrap.service'; -import { ScrapController } from './scrap.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ScrapBoxEntity } from 'src/entities'; +import { ScrapController } from './scrap.controller'; +import { ScrapService } from './scrap.service'; @Module({ imports: [TypeOrmModule.forFeature([ScrapBoxEntity])], diff --git a/src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto.ts b/src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto.ts index c4ede53..653988b 100644 --- a/src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto.ts +++ b/src/domain/subscribe/dtos/subscribeBoxResponseWithNotices.dto.ts @@ -1,5 +1,5 @@ -import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; import { ApiProperty } from '@nestjs/swagger'; +import { NoticeListResponseDto } from 'src/domain/notice/dtos/NoticeListResponse.dto'; import { Notice, ScrapBoxEntity, SubscribeBoxEntity } from 'src/entities'; import { SubscribeBoxResponseDto } from './subscribeBoxResponse.dto'; diff --git a/src/domain/subscribe/subscribe.module.ts b/src/domain/subscribe/subscribe.module.ts index acbb7cd..496986d 100644 --- a/src/domain/subscribe/subscribe.module.ts +++ b/src/domain/subscribe/subscribe.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { SubscribeService } from './subscribe.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { CategoryEntity, CategoryPerSubscribeBoxEntity, @@ -7,8 +7,8 @@ import { ScrapBoxEntity, SubscribeBoxEntity, } from 'src/entities'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { SubscribeController } from './subscribe.controller'; +import { SubscribeService } from './subscribe.service'; @Module({ controllers: [SubscribeController], diff --git a/src/domain/users/users.service.ts b/src/domain/users/users.service.ts index 09b0571..9a7e7a0 100644 --- a/src/domain/users/users.service.ts +++ b/src/domain/users/users.service.ts @@ -1,9 +1,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { hash } from 'bcrypt'; import { KudogUser, ProviderBookmark } from 'src/entities'; import { Repository } from 'typeorm'; import { ModifyInfoRequestDto, UserInfoResponseDto } from './dtos/userInfo.dto'; -import { hash } from 'bcrypt'; @Injectable() export class UsersService { diff --git a/src/entities/categoryPerSubscribes.entity.ts b/src/entities/categoryPerSubscribes.entity.ts index 7268641..b6955ea 100644 --- a/src/entities/categoryPerSubscribes.entity.ts +++ b/src/entities/categoryPerSubscribes.entity.ts @@ -1,5 +1,5 @@ -import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { CategoryEntity, SubscribeBoxEntity } from 'src/entities'; +import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; @Entity('category_per_subscribeBox') export class CategoryPerSubscribeBoxEntity { @ManyToOne( diff --git a/src/entities/notice.entity.ts b/src/entities/notice.entity.ts index 9076f56..d71498e 100644 --- a/src/entities/notice.entity.ts +++ b/src/entities/notice.entity.ts @@ -1,3 +1,4 @@ +import { CategoryEntity, ScrapEntity } from 'src/entities'; import { Column, Entity, @@ -5,7 +6,6 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { CategoryEntity, ScrapEntity } from 'src/entities'; @Entity('notice') export class Notice { diff --git a/src/entities/notificationToken.entity.ts b/src/entities/notificationToken.entity.ts index b728da4..e75a0f9 100644 --- a/src/entities/notificationToken.entity.ts +++ b/src/entities/notificationToken.entity.ts @@ -1,11 +1,11 @@ import { KudogUser } from 'src/entities'; import { - Entity, Column, + Entity, + JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId, - JoinColumn, } from 'typeorm'; @Entity('notification_token') diff --git a/src/entities/provider.entity.ts b/src/entities/provider.entity.ts index 2e1d249..605fe2b 100644 --- a/src/entities/provider.entity.ts +++ b/src/entities/provider.entity.ts @@ -1,5 +1,5 @@ -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { CategoryEntity } from 'src/entities'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { ProviderBookmark } from './providerBookmark.entity'; @Entity('provider') diff --git a/src/entities/providerBookmark.entity.ts b/src/entities/providerBookmark.entity.ts index 98a13d3..a9bf619 100644 --- a/src/entities/providerBookmark.entity.ts +++ b/src/entities/providerBookmark.entity.ts @@ -1,5 +1,5 @@ +import { KudogUser, ProviderEntity } from 'src/entities'; import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { ProviderEntity, KudogUser } from 'src/entities'; @Entity('provider_bookmark') export class ProviderBookmark { diff --git a/src/entities/scrap.entity.ts b/src/entities/scrap.entity.ts index 3901c94..b54d957 100644 --- a/src/entities/scrap.entity.ts +++ b/src/entities/scrap.entity.ts @@ -1,3 +1,4 @@ +import { Notice } from 'src/entities'; import { Column, Entity, @@ -6,7 +7,6 @@ import { PrimaryGeneratedColumn, RelationId, } from 'typeorm'; -import { Notice } from 'src/entities'; import { ScrapBoxEntity } from './scrapBox.entity'; @Entity('scrap') diff --git a/src/entities/scrapBox.entity.ts b/src/entities/scrapBox.entity.ts index e99236d..080fd51 100644 --- a/src/entities/scrapBox.entity.ts +++ b/src/entities/scrapBox.entity.ts @@ -1,3 +1,4 @@ +import { KudogUser, ScrapEntity } from 'src/entities'; import { Column, Entity, @@ -7,7 +8,6 @@ import { PrimaryGeneratedColumn, RelationId, } from 'typeorm'; -import { KudogUser, ScrapEntity } from 'src/entities'; @Entity('scrap_box') export class ScrapBoxEntity { diff --git a/src/entities/subscribeBox.entity.ts b/src/entities/subscribeBox.entity.ts index a8b08d7..7d4c6c1 100644 --- a/src/entities/subscribeBox.entity.ts +++ b/src/entities/subscribeBox.entity.ts @@ -1,3 +1,4 @@ +import { KudogUser } from 'src/entities'; import { Column, Entity, @@ -7,7 +8,6 @@ import { PrimaryGeneratedColumn, RelationId, } from 'typeorm'; -import { KudogUser } from 'src/entities'; import { CategoryPerSubscribeBoxEntity } from './categoryPerSubscribes.entity'; @Entity('subscribe_box') diff --git a/src/middlewares/auth.middleware.spec.ts b/src/middlewares/auth.middleware.spec.ts index fb6de85..23e4884 100644 --- a/src/middlewares/auth.middleware.spec.ts +++ b/src/middlewares/auth.middleware.spec.ts @@ -54,7 +54,6 @@ describe('authMiddleware', () => { const payload: JwtPayload = { id: 1, name: 'test', - signedAt: Date.now().toString(), }; const options = { expiresIn: -200 }; const token = jwt.sign(payload, JWT_SECRET_KEY, options); diff --git a/src/pipes/intValidation.pipe.ts b/src/pipes/intValidation.pipe.ts index 1e4ff24..6aa65c9 100644 --- a/src/pipes/intValidation.pipe.ts +++ b/src/pipes/intValidation.pipe.ts @@ -1,15 +1,15 @@ import { - PipeTransform, - Injectable, ArgumentMetadata, + Injectable, NotAcceptableException, + PipeTransform, } from '@nestjs/common'; @Injectable() export class IntValidationPipe implements PipeTransform { transform(value: string, metadata: ArgumentMetadata): number { - const val = parseInt(value, 10); - if (isNaN(val)) { + const val = Number.parseInt(value, 10); + if (Number.isNaN(val)) { throw new NotAcceptableException( `입력값이 유효하지 않습니다 - <${metadata.data}> 해당 변수는 정수여야 합니다.`, ); diff --git a/src/pipes/stringValidation.pipe.ts b/src/pipes/stringValidation.pipe.ts index 69895d8..b85e804 100644 --- a/src/pipes/stringValidation.pipe.ts +++ b/src/pipes/stringValidation.pipe.ts @@ -1,8 +1,8 @@ import { - PipeTransform, - Injectable, ArgumentMetadata, + Injectable, NotAcceptableException, + PipeTransform, } from '@nestjs/common'; @Injectable() From 734071fa1d6abc2e3e9b0d9cd80329ddb8cb3706 Mon Sep 17 00:00:00 2001 From: overthestream Date: Mon, 29 Jul 2024 23:18:28 +0900 Subject: [PATCH 09/11] remove not used imporst --- src/domain/auth/entities/changePwd.entity.ts | 1 - src/domain/auth/guards/jwt.guard.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/domain/auth/entities/changePwd.entity.ts b/src/domain/auth/entities/changePwd.entity.ts index 6fc4334..fb08c8c 100644 --- a/src/domain/auth/entities/changePwd.entity.ts +++ b/src/domain/auth/entities/changePwd.entity.ts @@ -5,7 +5,6 @@ import { Index, JoinColumn, ManyToOne, - OneToOne, PrimaryGeneratedColumn, RelationId, } from 'typeorm'; diff --git a/src/domain/auth/guards/jwt.guard.ts b/src/domain/auth/guards/jwt.guard.ts index 2622c28..ee789f9 100644 --- a/src/domain/auth/guards/jwt.guard.ts +++ b/src/domain/auth/guards/jwt.guard.ts @@ -1,5 +1,4 @@ import { ApiKudogExceptionResponse } from '@/common/decorators'; -import type { JwtPayload } from '@/common/types/auth'; import { throwKudogException } from '@/common/utils/exception'; import { type CanActivate, From 96c1e8388f31d72a6a31613e5191de572ec1b387 Mon Sep 17 00:00:00 2001 From: overthestream Date: Mon, 29 Jul 2024 23:21:55 +0900 Subject: [PATCH 10/11] add index for code --- src/domain/auth/entities/emailAuthentication.entity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/auth/entities/emailAuthentication.entity.ts b/src/domain/auth/entities/emailAuthentication.entity.ts index a687879..ad6b1c1 100644 --- a/src/domain/auth/entities/emailAuthentication.entity.ts +++ b/src/domain/auth/entities/emailAuthentication.entity.ts @@ -21,6 +21,7 @@ export class EmailAuthenticationEntity { @Column({ default: false }) authenticated!: boolean; + @Index() @Column() code!: string; } From a85e780e3a8abafd98b7eb7f1ce84dd65b71d341 Mon Sep 17 00:00:00 2001 From: overthestream Date: Mon, 5 Aug 2024 20:13:58 +0900 Subject: [PATCH 11/11] refactor category domain --- src/app.module.ts | 1 + .../decorators/docs/category.decorator.ts | 10 ++--- src/common/decorators/docs/index.ts | 1 + src/common/decorators/useValidation.ts | 4 ++ src/common/dtos/findOneParams.dto.ts | 11 +++++ src/common/utils/exception.ts | 1 - src/domain/auth/auth.module.ts | 10 ++--- src/domain/auth/auth.repository.ts | 9 ++-- src/domain/auth/auth.service.ts | 2 +- src/domain/auth/entities/changePwd.entity.ts | 7 ++-- .../auth/entities/refreshToken.entity.ts | 10 ++--- src/domain/category/category.controller.ts | 11 +++-- src/domain/category/category.module.ts | 9 ++-- src/domain/category/category.repository.ts | 42 +++++++++++++++++++ src/domain/category/category.service.ts | 33 +++++---------- .../category}/entities/category.entity.ts | 13 ++++-- .../category}/entities/provider.entity.ts | 8 ++-- .../entities/providerBookmark.entity.ts | 11 ++--- src/domain/mail/mail.controller.ts | 2 +- src/domain/mail/mail.module.ts | 4 +- src/domain/mail/mail.service.ts | 10 ++--- .../notice}/entities/notice.entity.ts | 0 .../entities/categoryPerSubscribes.entity.ts | 0 src/domain/users/dtos/userInfo.dto.ts | 10 ++--- .../users}/entities/kudogUser.entity.ts | 10 ++--- src/domain/users/user.repository.ts | 21 ++++++---- src/domain/users/users.module.ts | 6 ++- src/domain/users/users.service.ts | 34 ++++++++------- src/entities/index.ts | 12 +++--- src/entities/notification.entity.ts | 8 ++-- src/entities/notificationToken.entity.ts | 8 ++-- src/entities/scrap.entity.ts | 2 +- src/entities/scrapBox.entity.ts | 6 +-- src/entities/subscribeBox.entity.ts | 10 ++--- src/fetch/fetch.module.ts | 4 +- 35 files changed, 202 insertions(+), 138 deletions(-) create mode 100644 src/common/dtos/findOneParams.dto.ts create mode 100644 src/domain/category/category.repository.ts rename src/{ => domain/category}/entities/category.entity.ts (61%) rename src/{ => domain/category}/entities/provider.entity.ts (65%) rename src/{ => domain/category}/entities/providerBookmark.entity.ts (67%) rename src/{ => domain/notice}/entities/notice.entity.ts (100%) rename src/{ => domain/subscribe}/entities/categoryPerSubscribes.entity.ts (100%) rename src/{ => domain/users}/entities/kudogUser.entity.ts (80%) diff --git a/src/app.module.ts b/src/app.module.ts index 8b5387a..6c4d344 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -33,6 +33,7 @@ import { AuthMiddleware } from './middlewares/auth.middleware'; autoLoadEntities: true, logging: true, logger: new FileLogger('all', { logPath: './logs/orm.log' }), + synchronize: true, }), MailerModule.forRoot({ transport: { diff --git a/src/common/decorators/docs/category.decorator.ts b/src/common/decorators/docs/category.decorator.ts index b00df7f..4a27f92 100644 --- a/src/common/decorators/docs/category.decorator.ts +++ b/src/common/decorators/docs/category.decorator.ts @@ -1,4 +1,5 @@ import { ApiKudogExceptionResponse } from '@/common/decorators'; +import { FindOneParams } from '@/common/dtos/findOneParams.dto'; import type { MethodNames } from '@/common/types/method'; import type { CategoryController } from '@/domain/category/category.controller'; import { ProviderListResponseDto } from '@/domain/category/dtos/ProviderListResponse.dto'; @@ -17,7 +18,6 @@ const CategoryDocsMap: Record = { description: '학부 리스트', type: [ProviderListResponseDto], }), - ApiKudogExceptionResponse([]), ], getCategories: [ ApiOperation({ @@ -26,17 +26,14 @@ const CategoryDocsMap: Record = { 'DB의 학부 소속 카테고리 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', }), ApiParam({ - name: 'id', description: '학부 id', - type: Number, - required: true, - example: 1, + type: FindOneParams, + name: 'id', }), ApiOkResponse({ description: '스크랩학부 소속 카테고리들', type: [CategoryListResponseDto], }), - ApiKudogExceptionResponse([]), ], getBookmarkedProviders: [ ApiOperation({ @@ -48,7 +45,6 @@ const CategoryDocsMap: Record = { description: '사용자의 즐겨찾는 학과 목록', type: [ProviderListResponseDto], }), - ApiKudogExceptionResponse([]), ], }; diff --git a/src/common/decorators/docs/index.ts b/src/common/decorators/docs/index.ts index e380fe4..344c5f2 100644 --- a/src/common/decorators/docs/index.ts +++ b/src/common/decorators/docs/index.ts @@ -1,5 +1,6 @@ export { AuthDocs } from './auth.decorator'; export { CategoryDocs } from './category.decorator'; +export * from './common.decorator'; export { MailDocs } from './mail.decorator'; export { NoticeDocs } from './notice.decorator'; export { NotificationDocs } from './notification.decorator'; diff --git a/src/common/decorators/useValidation.ts b/src/common/decorators/useValidation.ts index e487597..12938cb 100644 --- a/src/common/decorators/useValidation.ts +++ b/src/common/decorators/useValidation.ts @@ -7,7 +7,11 @@ export function UseValidation(exceptions: ExceptionNames[]): MethodDecorator { UsePipes( new ValidationPipe({ transform: true, + //whitelist: true, exceptionFactory(errs) { + console.log(errs[0].value); + console.log(typeof errs[0].value); + console.log(errs[0].constraints); const err = Object.values(errs[0].contexts)[0].exception; throwKudogException(err); }, diff --git a/src/common/dtos/findOneParams.dto.ts b/src/common/dtos/findOneParams.dto.ts new file mode 100644 index 0000000..49399f9 --- /dev/null +++ b/src/common/dtos/findOneParams.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { IsInt, IsPositive } from 'class-validator'; + +export class FindOneParams { + @IsPositive({ context: { exception: 'NOT_ACCEPTABLE' } }) + @IsInt({ context: { exception: 'NOT_ACCEPTABLE' } }) + @Transform(({ value }) => Number(value)) + @ApiProperty({ example: 1 }) + id: number; +} diff --git a/src/common/utils/exception.ts b/src/common/utils/exception.ts index 1974ae4..dcc61a9 100644 --- a/src/common/utils/exception.ts +++ b/src/common/utils/exception.ts @@ -8,7 +8,6 @@ interface KudogErrorResponse { } export type ExceptionNames = keyof typeof ERROR; - export const EXCEPTIONS: { [key in ExceptionNames]: KudogErrorResponse } = ERROR; diff --git a/src/domain/auth/auth.module.ts b/src/domain/auth/auth.module.ts index 0f82bc7..41edee0 100644 --- a/src/domain/auth/auth.module.ts +++ b/src/domain/auth/auth.module.ts @@ -1,17 +1,15 @@ import { ChannelModule } from '@/domain/channel/channel.module'; +import { UsersModule } from '@/domain/users/users.module'; import { MailerModule } from '@nestjs-modules/mailer'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { - ChangePwdAuthenticationEntity, - EmailAuthenticationEntity, - RefreshTokenEntity, -} from 'src/entities'; -import { UsersModule } from '../users/users.module'; import { AuthController } from './auth.controller'; import { AuthRepository } from './auth.repository'; import { AuthService } from './auth.service'; +import { ChangePwdAuthenticationEntity } from './entities/changePwd.entity'; +import { EmailAuthenticationEntity } from './entities/emailAuthentication.entity'; +import { RefreshTokenEntity } from './entities/refreshToken.entity'; @Module({ imports: [ TypeOrmModule.forFeature([ diff --git a/src/domain/auth/auth.repository.ts b/src/domain/auth/auth.repository.ts index 87c0c6f..e9f018e 100644 --- a/src/domain/auth/auth.repository.ts +++ b/src/domain/auth/auth.repository.ts @@ -1,13 +1,10 @@ -import { - ChangePwdAuthenticationEntity, - EmailAuthenticationEntity, - RefreshTokenEntity, -} from '@/entities'; import { Injectable } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { InjectDataSource } from '@nestjs/typeorm'; import { type DataSource, LessThan, Repository } from 'typeorm'; - +import { ChangePwdAuthenticationEntity } from './entities/changePwd.entity'; +import { EmailAuthenticationEntity } from './entities/emailAuthentication.entity'; +import { RefreshTokenEntity } from './entities/refreshToken.entity'; @Injectable() export class AuthRepository { private changePwdAuthEntityRepository: Repository; diff --git a/src/domain/auth/auth.service.ts b/src/domain/auth/auth.service.ts index 0221e91..f19c8b7 100644 --- a/src/domain/auth/auth.service.ts +++ b/src/domain/auth/auth.service.ts @@ -1,11 +1,11 @@ import { JwtPayload } from '@/common/types/auth'; import { throwKudogException } from '@/common/utils/exception'; import { ChannelService } from '@/domain/channel/channel.service'; +import { UserRepository } from '@/domain/users/user.repository'; import { MailerService } from '@nestjs-modules/mailer'; import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { compare, hash } from 'bcrypt'; -import { UserRepository } from '../users/user.repository'; import { AuthRepository } from './auth.repository'; import { ChangePasswordDto, diff --git a/src/domain/auth/entities/changePwd.entity.ts b/src/domain/auth/entities/changePwd.entity.ts index fb08c8c..25df8df 100644 --- a/src/domain/auth/entities/changePwd.entity.ts +++ b/src/domain/auth/entities/changePwd.entity.ts @@ -1,3 +1,4 @@ +import { KudogUserEntity } from '@/domain/users/entities/kudogUser.entity'; import { Column, CreateDateColumn, @@ -8,7 +9,6 @@ import { PrimaryGeneratedColumn, RelationId, } from 'typeorm'; -import { KudogUser } from '../../../entities/kudogUser.entity'; @Entity('change_pwd_authentication') export class ChangePwdAuthenticationEntity { @@ -16,12 +16,13 @@ export class ChangePwdAuthenticationEntity { id!: number; @ManyToOne( - () => KudogUser, + () => KudogUserEntity, () => undefined, { onDelete: 'CASCADE' }, ) @JoinColumn({ name: 'userId' }) - user!: KudogUser; + user!: KudogUserEntity; + @Column() @RelationId((entity: ChangePwdAuthenticationEntity) => entity.user) userId!: number; diff --git a/src/domain/auth/entities/refreshToken.entity.ts b/src/domain/auth/entities/refreshToken.entity.ts index c42cfa3..b00089a 100644 --- a/src/domain/auth/entities/refreshToken.entity.ts +++ b/src/domain/auth/entities/refreshToken.entity.ts @@ -1,4 +1,4 @@ -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from '@/domain/users/entities/kudogUser.entity'; import { Column, CreateDateColumn, @@ -16,20 +16,20 @@ export class RefreshTokenEntity { id!: number; @ManyToOne( - () => KudogUser, + () => KudogUserEntity, (user) => user.refreshTokens, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'userId' }) - user!: KudogUser; - + user!: KudogUserEntity; + @Column() @RelationId((refreshToken: RefreshTokenEntity) => refreshToken.user) userId!: number; @Index() - @Column({ type: 'varchar', length: 255 }) + @Column() token!: string; @CreateDateColumn() diff --git a/src/domain/category/category.controller.ts b/src/domain/category/category.controller.ts index 9a61782..842fe9d 100644 --- a/src/domain/category/category.controller.ts +++ b/src/domain/category/category.controller.ts @@ -1,8 +1,9 @@ import { InjectUser, NamedController } from '@/common/decorators'; -import { CategoryDocs } from '@/common/decorators/docs/category.decorator'; +import { CategoryDocs } from '@/common/decorators/docs'; +import { UseValidation } from '@/common/decorators/useValidation'; +import { FindOneParams } from '@/common/dtos/findOneParams.dto'; import { JwtPayload } from '@/common/types/auth'; -import { IntValidationPipe } from '@/pipes/intValidation.pipe'; -import { Get, Param } from '@nestjs/common'; +import { Body, Get, Param, UsePipes, ValidationPipe } from '@nestjs/common'; import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { CategoryService } from './category.service'; import { ProviderListResponseDto } from './dtos/ProviderListResponse.dto'; @@ -20,10 +21,12 @@ export class CategoryController { } @UseJwtGuard() + @UseValidation(['NOT_ACCEPTABLE']) @Get('/by-providers/:id') async getCategories( - @Param('id', new IntValidationPipe()) id: number, + @Param() params: FindOneParams, ): Promise { + const { id } = params; return this.categoryService.getCategories(id); } diff --git a/src/domain/category/category.module.ts b/src/domain/category/category.module.ts index e61b84f..f20861c 100644 --- a/src/domain/category/category.module.ts +++ b/src/domain/category/category.module.ts @@ -1,18 +1,21 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CategoryEntity, ProviderBookmark, ProviderEntity } from 'src/entities'; import { CategoryController } from './category.controller'; +import { CategoryRepository } from './category.repository'; import { CategoryService } from './category.service'; +import { CategoryEntity } from './entities/category.entity'; +import { ProviderEntity } from './entities/provider.entity'; +import { ProviderBookmarkEntity } from './entities/providerBookmark.entity'; @Module({ imports: [ TypeOrmModule.forFeature([ ProviderEntity, - ProviderBookmark, + ProviderBookmarkEntity, CategoryEntity, ]), ], controllers: [CategoryController], - providers: [CategoryService], + providers: [CategoryService, CategoryRepository], }) export class CategoryModule {} diff --git a/src/domain/category/category.repository.ts b/src/domain/category/category.repository.ts new file mode 100644 index 0000000..6adb7f1 --- /dev/null +++ b/src/domain/category/category.repository.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource } from '@nestjs/typeorm'; +import type { DataSource, Repository } from 'typeorm'; +import { CategoryEntity } from './entities/category.entity'; +import { ProviderEntity } from './entities/provider.entity'; +import { ProviderBookmarkEntity } from './entities/providerBookmark.entity'; + +@Injectable() +export class CategoryRepository { + private categoryEntityRepository: Repository; + private providerEntityRepository: Repository; + private providerBookmarkEntityRepository: Repository; + + constructor(@InjectDataSource() private readonly dataSource: DataSource) { + this.categoryEntityRepository = + this.dataSource.getRepository(CategoryEntity); + this.providerEntityRepository = + this.dataSource.getRepository(ProviderEntity); + this.providerBookmarkEntityRepository = this.dataSource.getRepository( + ProviderBookmarkEntity, + ); + } + + async getProvidersJoinCategories(): Promise { + return this.providerEntityRepository.find({ + relations: ['categories'], + }); + } + + async getCategoriesByProviderId( + providerId: number, + ): Promise { + return this.categoryEntityRepository.find({ where: { providerId } }); + } + + async getBookmarkedProvidersJoinCategories(userId: number) { + return this.providerBookmarkEntityRepository.find({ + where: { userId }, + relations: ['provider', 'provider.categories'], + }); + } +} diff --git a/src/domain/category/category.service.ts b/src/domain/category/category.service.ts index c7bdd45..5046f2e 100644 --- a/src/domain/category/category.service.ts +++ b/src/domain/category/category.service.ts @@ -1,32 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { CategoryEntity, ProviderBookmark, ProviderEntity } from 'src/entities'; -import { Repository } from 'typeorm'; +import { CategoryRepository } from './category.repository'; import { ProviderListResponseDto } from './dtos/ProviderListResponse.dto'; import { CategoryListResponseDto } from './dtos/categoryListResponse.dto'; @Injectable() export class CategoryService { - constructor( - @InjectRepository(ProviderEntity) - private readonly providerRepository: Repository, - @InjectRepository(ProviderBookmark) - private readonly providerBookmarkRepository: Repository, - @InjectRepository(CategoryEntity) - private readonly categoryRepository: Repository, - ) {} + constructor(private readonly categoryRepository: CategoryRepository) {} async getProviders(): Promise { - const providers = await this.providerRepository.find({ - relations: ['categories'], - }); + const providers = + await this.categoryRepository.getProvidersJoinCategories(); return providers.map((provider) => new ProviderListResponseDto(provider)); } - async getCategories(id: number): Promise { - const categories = await this.categoryRepository.find({ - where: { provider: { id } }, - }); + async getCategories(providerId: number): Promise { + const categories = + await this.categoryRepository.getCategoriesByProviderId(providerId); return categories.map((category) => new CategoryListResponseDto(category)); } @@ -34,10 +23,10 @@ export class CategoryService { async getBookmarkedProviders( userId: number, ): Promise { - const bookmarks = await this.providerBookmarkRepository.find({ - where: { user: { id: userId } }, - relations: ['provider', 'provider.categories'], - }); + const bookmarks = + await this.categoryRepository.getBookmarkedProvidersJoinCategories( + userId, + ); return bookmarks.map( (bookmark) => new ProviderListResponseDto(bookmark.provider), diff --git a/src/entities/category.entity.ts b/src/domain/category/entities/category.entity.ts similarity index 61% rename from src/entities/category.entity.ts rename to src/domain/category/entities/category.entity.ts index b7cd88b..3f5fe30 100644 --- a/src/entities/category.entity.ts +++ b/src/domain/category/entities/category.entity.ts @@ -1,13 +1,15 @@ -import { Notice } from 'src/entities'; -import { ProviderEntity } from 'src/entities'; +import { Notice } from '@/domain/notice/entities/notice.entity'; +import { CategoryPerSubscribeBoxEntity } from '@/domain/subscribe/entities/categoryPerSubscribes.entity'; import { Column, Entity, + JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn, + RelationId, } from 'typeorm'; -import { CategoryPerSubscribeBoxEntity } from './categoryPerSubscribes.entity'; +import { ProviderEntity } from './provider.entity'; @Entity('category') export class CategoryEntity { @@ -24,8 +26,13 @@ export class CategoryEntity { () => ProviderEntity, (provider) => provider.categories, ) + @JoinColumn({ name: 'providerId' }) provider: ProviderEntity; + @Column() + @RelationId((entity: CategoryEntity) => entity.provider) + providerId: number; + @OneToMany( () => Notice, (notice) => notice.category, diff --git a/src/entities/provider.entity.ts b/src/domain/category/entities/provider.entity.ts similarity index 65% rename from src/entities/provider.entity.ts rename to src/domain/category/entities/provider.entity.ts index 605fe2b..93b4945 100644 --- a/src/entities/provider.entity.ts +++ b/src/domain/category/entities/provider.entity.ts @@ -1,6 +1,6 @@ -import { CategoryEntity } from 'src/entities'; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { ProviderBookmark } from './providerBookmark.entity'; +import { CategoryEntity } from './category.entity'; +import { ProviderBookmarkEntity } from './providerBookmark.entity'; @Entity('provider') export class ProviderEntity { @@ -17,8 +17,8 @@ export class ProviderEntity { categories: CategoryEntity[]; @OneToMany( - () => ProviderBookmark, + () => ProviderBookmarkEntity, (bookmark) => bookmark.provider, ) - bookmarks: ProviderBookmark[]; + bookmarks: ProviderBookmarkEntity[]; } diff --git a/src/entities/providerBookmark.entity.ts b/src/domain/category/entities/providerBookmark.entity.ts similarity index 67% rename from src/entities/providerBookmark.entity.ts rename to src/domain/category/entities/providerBookmark.entity.ts index a9bf619..064460c 100644 --- a/src/entities/providerBookmark.entity.ts +++ b/src/domain/category/entities/providerBookmark.entity.ts @@ -1,8 +1,9 @@ -import { KudogUser, ProviderEntity } from 'src/entities'; +import { KudogUserEntity } from '@/domain/users/entities/kudogUser.entity'; import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; +import { ProviderEntity } from './provider.entity'; @Entity('provider_bookmark') -export class ProviderBookmark { +export class ProviderBookmarkEntity { @ManyToOne( () => ProviderEntity, (provider) => provider.bookmarks, @@ -16,14 +17,14 @@ export class ProviderBookmark { providerId: number; @ManyToOne( - () => KudogUser, - (user) => user.providerBookmarks, + () => KudogUserEntity, + (user) => user.ProviderBookmarkEntitys, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'user_id' }) - user: KudogUser; + user: KudogUserEntity; @PrimaryColumn({ name: 'user_id', type: 'integer' }) userId: number; } diff --git a/src/domain/mail/mail.controller.ts b/src/domain/mail/mail.controller.ts index 9b8c250..e7e9f44 100644 --- a/src/domain/mail/mail.controller.ts +++ b/src/domain/mail/mail.controller.ts @@ -1,5 +1,5 @@ import { NamedController } from '@/common/decorators'; -import { MailDocs } from '@/common/decorators/docs/mail.decorator'; +import { MailDocs } from '@/common/decorators/docs'; import { Body, Post } from '@nestjs/common'; import { verifyCodeRequestDto } from './dtos/verifyCodeRequest.dto'; import { verifyRequestDto } from './dtos/verifyRequest.dto'; diff --git a/src/domain/mail/mail.module.ts b/src/domain/mail/mail.module.ts index fbdff74..1bae544 100644 --- a/src/domain/mail/mail.module.ts +++ b/src/domain/mail/mail.module.ts @@ -3,7 +3,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EmailAuthenticationEntity, - KudogUser, + KudogUserEntity, Notice, SubscribeBoxEntity, } from 'src/entities'; @@ -14,7 +14,7 @@ import { MailService } from './mail.service'; imports: [ TypeOrmModule.forFeature([ EmailAuthenticationEntity, - KudogUser, + KudogUserEntity, SubscribeBoxEntity, Notice, ]), diff --git a/src/domain/mail/mail.service.ts b/src/domain/mail/mail.service.ts index 6d0e316..a86add7 100644 --- a/src/domain/mail/mail.service.ts +++ b/src/domain/mail/mail.service.ts @@ -11,7 +11,7 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; import { EmailAuthenticationEntity, - KudogUser, + KudogUserEntity, Notice, SubscribeBoxEntity, } from 'src/entities'; @@ -23,8 +23,8 @@ export class MailService { private readonly mailerService: MailerService, @InjectRepository(EmailAuthenticationEntity) private readonly emailAuthenticationRepository: Repository, - @InjectRepository(KudogUser) - private readonly kudogUserRepository: Repository, + @InjectRepository(KudogUserEntity) + private readonly KudogUserEntityRepository: Repository, @InjectRepository(SubscribeBoxEntity) private readonly subscribeBoxRepository: Repository, @InjectRepository(Notice) @@ -41,7 +41,7 @@ export class MailService { } async sendVerificationCode(to: string): Promise { - const existingUser = await this.kudogUserRepository.findOne({ + const existingUser = await this.KudogUserEntityRepository.findOne({ where: { email: to }, }); if (existingUser) throw new ConflictException('사용중인 이메일입니다.'); @@ -83,7 +83,7 @@ export class MailService { } async checkVerificationCode(email: string, code: string): Promise { - const existingUser = await this.kudogUserRepository.findOne({ + const existingUser = await this.KudogUserEntityRepository.findOne({ where: { email: email }, }); if (existingUser) throw new ConflictException('사용중인 이메일입니다.'); diff --git a/src/entities/notice.entity.ts b/src/domain/notice/entities/notice.entity.ts similarity index 100% rename from src/entities/notice.entity.ts rename to src/domain/notice/entities/notice.entity.ts diff --git a/src/entities/categoryPerSubscribes.entity.ts b/src/domain/subscribe/entities/categoryPerSubscribes.entity.ts similarity index 100% rename from src/entities/categoryPerSubscribes.entity.ts rename to src/domain/subscribe/entities/categoryPerSubscribes.entity.ts diff --git a/src/domain/users/dtos/userInfo.dto.ts b/src/domain/users/dtos/userInfo.dto.ts index 100d40b..b582e28 100644 --- a/src/domain/users/dtos/userInfo.dto.ts +++ b/src/domain/users/dtos/userInfo.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, PartialType, PickType } from '@nestjs/swagger'; import { SignupRequestDto } from 'src/domain/auth/dtos/signupRequest.dto'; -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from 'src/entities'; export class ModifyInfoRequestDto extends PartialType( PickType(SignupRequestDto, ['email', 'name', 'password'] as const), @@ -15,7 +15,7 @@ export class ModifyInfoRequestDto extends PartialType( description: '즐겨찾는 학과 수정 by id', example: ['14', '23', '11'], }) - providerBookmarks?: number[]; + ProviderBookmarkEntitys?: number[]; } export class UserInfoResponseDto { @@ -29,13 +29,13 @@ export class UserInfoResponseDto { sendTime: string; @ApiProperty({ example: ['미디어학부', 'KUPID', '정보대학'] }) - providerBookmarks: string[]; + ProviderBookmarkEntitys: string[]; - constructor(entity: KudogUser) { + constructor(entity: KudogUserEntity) { this.name = entity.name; this.email = entity.email; this.sendTime = entity.sendTime; - this.providerBookmarks = entity.providerBookmarks.map( + this.ProviderBookmarkEntitys = entity.ProviderBookmarkEntitys.map( (bookmark) => bookmark.provider.name, ); } diff --git a/src/entities/kudogUser.entity.ts b/src/domain/users/entities/kudogUser.entity.ts similarity index 80% rename from src/entities/kudogUser.entity.ts rename to src/domain/users/entities/kudogUser.entity.ts index 9b572de..829fda0 100644 --- a/src/entities/kudogUser.entity.ts +++ b/src/domain/users/entities/kudogUser.entity.ts @@ -12,10 +12,10 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; -import { ProviderBookmark } from './providerBookmark.entity'; +import { ProviderBookmarkEntity } from '../../category/entities/providerBookmark.entity'; @Entity('kudog_user') -export class KudogUser { +export class KudogUserEntity { @PrimaryGeneratedColumn() id: number; @@ -60,10 +60,10 @@ export class KudogUser { refreshTokens: RefreshTokenEntity[]; @OneToMany( - () => ProviderBookmark, - (providerBookmark) => providerBookmark.user, + () => ProviderBookmarkEntity, + (ProviderBookmarkEntity) => ProviderBookmarkEntity.user, ) - providerBookmarks: ProviderBookmark[]; + ProviderBookmarkEntitys: ProviderBookmarkEntity[]; @Column({ default: '18:00' }) sendTime: string; diff --git a/src/domain/users/user.repository.ts b/src/domain/users/user.repository.ts index 980f361..20b1577 100644 --- a/src/domain/users/user.repository.ts +++ b/src/domain/users/user.repository.ts @@ -1,25 +1,25 @@ -import { KudogUser } from '@/entities'; +import { KudogUserEntity } from '@/entities'; import { Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { type DataSource, type FindManyOptions, Repository } from 'typeorm'; @Injectable() export class UserRepository { - private entityRepository: Repository; + private entityRepository: Repository; constructor(@InjectDataSource() private dataSource: DataSource) { - this.entityRepository = this.dataSource.getRepository(KudogUser); + this.entityRepository = this.dataSource.getRepository(KudogUserEntity); } - async findById(id: number): Promise { + async findById(id: number): Promise { return this.entityRepository.findOne({ where: { id } }); } - async findByEmail(email: string): Promise { + async findByEmail(email: string): Promise { return this.entityRepository.findOne({ where: { email } }); } - async remove(user: KudogUser): Promise { + async remove(user: KudogUserEntity): Promise { return this.entityRepository.remove(user); } @@ -27,16 +27,19 @@ export class UserRepository { email: string, name: string, passwordHash: string, - ): Promise { + ): Promise { const entity = this.entityRepository.create({ email, name, passwordHash }); return this.entityRepository.save(entity); } - async count(options?: FindManyOptions): Promise { + async count(options?: FindManyOptions): Promise { return this.entityRepository.count(options); } - async changePwd(entity: KudogUser, passwordHash: string): Promise { + async changePwd( + entity: KudogUserEntity, + passwordHash: string, + ): Promise { entity.passwordHash = passwordHash; await this.entityRepository.save(entity); } diff --git a/src/domain/users/users.module.ts b/src/domain/users/users.module.ts index 8b5e085..a40888e 100644 --- a/src/domain/users/users.module.ts +++ b/src/domain/users/users.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { KudogUser, ProviderBookmark } from 'src/entities'; +import { KudogUserEntity, ProviderBookmarkEntity } from 'src/entities'; import { UserRepository } from './user.repository'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @@ -9,6 +9,8 @@ import { UsersService } from './users.service'; controllers: [UsersController], providers: [UsersService, UserRepository], exports: [UserRepository], - imports: [TypeOrmModule.forFeature([KudogUser, ProviderBookmark])], + imports: [ + TypeOrmModule.forFeature([KudogUserEntity, ProviderBookmarkEntity]), + ], }) export class UsersModule {} diff --git a/src/domain/users/users.service.ts b/src/domain/users/users.service.ts index 9a7e7a0..809819d 100644 --- a/src/domain/users/users.service.ts +++ b/src/domain/users/users.service.ts @@ -1,24 +1,27 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { hash } from 'bcrypt'; -import { KudogUser, ProviderBookmark } from 'src/entities'; +import { KudogUserEntity, ProviderBookmarkEntity } from 'src/entities'; import { Repository } from 'typeorm'; import { ModifyInfoRequestDto, UserInfoResponseDto } from './dtos/userInfo.dto'; @Injectable() export class UsersService { constructor( - @InjectRepository(KudogUser) - private readonly userRepository: Repository, - @InjectRepository(ProviderBookmark) - private readonly providerBookmarkRepository: Repository, + @InjectRepository(KudogUserEntity) + private readonly userRepository: Repository, + @InjectRepository(ProviderBookmarkEntity) + private readonly ProviderBookmarkEntityRepository: Repository, ) {} saltOrRounds = 10; async getUserInfo(id: number): Promise { const user = await this.userRepository.findOne({ where: { id }, - relations: ['providerBookmarks', 'providerBookmarks.provider'], + relations: [ + 'ProviderBookmarkEntitys', + 'ProviderBookmarkEntitys.provider', + ], }); if (!user) throw new NotFoundException('존재하지 않는 유저입니다.'); @@ -41,17 +44,20 @@ export class UsersService { } if (dto.sendTime) user.sendTime = dto.sendTime; - if (dto.providerBookmarks) { - await this.providerBookmarkRepository.delete({ + if (dto.ProviderBookmarkEntitys) { + await this.ProviderBookmarkEntityRepository.delete({ user: { id }, }); - const providerBookmarks = dto.providerBookmarks.map((providerId) => - this.providerBookmarkRepository.create({ - user: { id }, - provider: { id: providerId }, - }), + const ProviderBookmarkEntitys = dto.ProviderBookmarkEntitys.map( + (providerId) => + this.ProviderBookmarkEntityRepository.create({ + user: { id }, + provider: { id: providerId }, + }), + ); + await this.ProviderBookmarkEntityRepository.insert( + ProviderBookmarkEntitys, ); - await this.providerBookmarkRepository.insert(providerBookmarks); } await this.userRepository.save(user); } diff --git a/src/entities/index.ts b/src/entities/index.ts index 4c84453..70062ad 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,14 +1,14 @@ -export * from './category.entity'; -export * from './kudogUser.entity'; +export * from '../domain/category/entities/category.entity'; +export * from '../domain/users/entities/kudogUser.entity'; export * from '../domain/auth/entities/emailAuthentication.entity'; -export * from './notice.entity'; -export * from './provider.entity'; +export * from '../domain/notice/entities/notice.entity'; +export * from '../domain/category/entities/provider.entity'; export * from '../domain/auth/entities/changePwd.entity'; export * from './scrap.entity'; export * from './scrapBox.entity'; export * from './subscribeBox.entity'; -export * from './categoryPerSubscribes.entity'; +export * from '../domain/subscribe/entities/categoryPerSubscribes.entity'; export * from './notificationToken.entity'; export * from './notification.entity'; export * from '../domain/auth/entities/refreshToken.entity'; -export * from './providerBookmark.entity'; +export * from '../domain/category/entities/providerBookmark.entity'; diff --git a/src/entities/notification.entity.ts b/src/entities/notification.entity.ts index d7c4d0d..3cc9f79 100644 --- a/src/entities/notification.entity.ts +++ b/src/entities/notification.entity.ts @@ -1,4 +1,4 @@ -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from 'src/entities'; import { Column, CreateDateColumn, @@ -15,15 +15,15 @@ export class NotificationEntity { id: number; @ManyToOne( - () => KudogUser, + () => KudogUserEntity, (user) => user.notifications, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'user_id' }) - user: KudogUser; - + user: KudogUserEntity; + @Column() @RelationId((notification: NotificationEntity) => notification.user) userId: number; diff --git a/src/entities/notificationToken.entity.ts b/src/entities/notificationToken.entity.ts index e75a0f9..7393575 100644 --- a/src/entities/notificationToken.entity.ts +++ b/src/entities/notificationToken.entity.ts @@ -1,4 +1,4 @@ -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from 'src/entities'; import { Column, Entity, @@ -14,15 +14,15 @@ export class NotificationTokenEntity { id: number; @ManyToOne( - () => KudogUser, + () => KudogUserEntity, (user) => user.notificationTokens, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'userId' }) - user: KudogUser; - + user: KudogUserEntity; + @Column() @RelationId( (notificationToken: NotificationTokenEntity) => notificationToken.user, ) diff --git a/src/entities/scrap.entity.ts b/src/entities/scrap.entity.ts index b54d957..f86cc83 100644 --- a/src/entities/scrap.entity.ts +++ b/src/entities/scrap.entity.ts @@ -33,7 +33,7 @@ export class ScrapEntity { ) @JoinColumn({ name: 'notice_id' }) notice: Notice; - + @Column() @RelationId((scrap: ScrapEntity) => scrap.notice) @Column({ name: 'notice_id' }) noticeId: number; diff --git a/src/entities/scrapBox.entity.ts b/src/entities/scrapBox.entity.ts index 080fd51..1ba6d7e 100644 --- a/src/entities/scrapBox.entity.ts +++ b/src/entities/scrapBox.entity.ts @@ -1,4 +1,4 @@ -import { KudogUser, ScrapEntity } from 'src/entities'; +import { KudogUserEntity, ScrapEntity } from 'src/entities'; import { Column, Entity, @@ -15,14 +15,14 @@ export class ScrapBoxEntity { id: number; @ManyToOne( - () => KudogUser, + () => KudogUserEntity, (user) => user.scrapBoxes, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'user_id' }) - user: KudogUser; + user: KudogUserEntity; @RelationId((scrapBox: ScrapBoxEntity) => scrapBox.user) @Column({ name: 'user_id' }) diff --git a/src/entities/subscribeBox.entity.ts b/src/entities/subscribeBox.entity.ts index 7d4c6c1..f33cdfe 100644 --- a/src/entities/subscribeBox.entity.ts +++ b/src/entities/subscribeBox.entity.ts @@ -1,4 +1,4 @@ -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from 'src/entities'; import { Column, Entity, @@ -8,7 +8,7 @@ import { PrimaryGeneratedColumn, RelationId, } from 'typeorm'; -import { CategoryPerSubscribeBoxEntity } from './categoryPerSubscribes.entity'; +import { CategoryPerSubscribeBoxEntity } from '../domain/subscribe/entities/categoryPerSubscribes.entity'; @Entity('subscribe_box') export class SubscribeBoxEntity { @@ -16,15 +16,15 @@ export class SubscribeBoxEntity { id: number; @ManyToOne( - () => KudogUser, + () => KudogUserEntity, (user) => user.subscribeBoxes, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'user_id' }) - user: KudogUser; - + user: KudogUserEntity; + @Column() @RelationId((subscribeBox: SubscribeBoxEntity) => subscribeBox.user) userId: number; diff --git a/src/fetch/fetch.module.ts b/src/fetch/fetch.module.ts index 10b13b2..52d6bb8 100644 --- a/src/fetch/fetch.module.ts +++ b/src/fetch/fetch.module.ts @@ -5,7 +5,7 @@ import { MailModule } from 'src/domain/mail/mail.module'; import { NotificationModule } from 'src/domain/notification/notification.module'; import { CategoryEntity, - KudogUser, + KudogUserEntity, Notice, ProviderEntity, } from 'src/entities'; @@ -18,7 +18,7 @@ import { FetchService } from './fetch.service'; Notice, ProviderEntity, CategoryEntity, - KudogUser, + KudogUserEntity, ]), MailModule, NotificationModule,