diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 1ab0df7..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 @@ -67,6 +66,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/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 diff --git a/package.json b/package.json index 3ebf261..a5f4e0e 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,14 +30,14 @@ "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", + "pino-pretty": "^11.2.2", "pinpoint-node-agent": "^0.8.4-next.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", @@ -52,10 +51,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", "jest": "^29.5.0", "source-map-support": "^0.5.21", 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 1777011..6c4d344 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,20 +1,24 @@ -import { Module } from '@nestjs/common'; +import { randomUUID } from 'node:crypto'; +import { MailerModule } from '@nestjs-modules/mailer'; +import { type MiddlewareConsumer, 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 { 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 { 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 { ChannelModule } from './domain/channel/channel.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'; +import { AuthMiddleware } from './middlewares/auth.middleware'; @Module({ imports: [ @@ -28,6 +32,8 @@ import { CategoryModule } from './domain/category/category.module'; database: process.env.DB_DATABASE, autoLoadEntities: true, logging: true, + logger: new FileLogger('all', { logPath: './logs/orm.log' }), + synchronize: true, }), MailerModule.forRoot({ transport: { @@ -40,6 +46,33 @@ 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: { + 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'; + }, + redact: ['req.body.password', 'req.headers.authorization'], + }, + }), MailModule, AuthModule, FetchModule, @@ -54,4 +87,8 @@ import { CategoryModule } from './domain/category/category.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 new file mode 100644 index 0000000..40ff63b --- /dev/null +++ b/src/asset/error.json @@ -0,0 +1,145 @@ +{ + "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": "이메일 또는 비밀번호가 일치하지 않습니다." + }, + "LOGIN_REQUIRED": { + "errorCode": 4011, + "statusCode": 401, + "name": "LOGIN_REQUIRED", + "message": "토큰이 없거나 만료되었습니다. 로그인해주세요." + }, + "JWT_TOKEN_EXPIRED": { + "errorCode": 4012, + "statusCode": 401, + "name": "JWT_TOKEN_EXPIRED", + "message": "JWT TOKEN이 만료되었습니다. 리프레시를 시도해주세요." + }, + "JWT_TOKEN_INVALID": { + "errorCode": 4013, + "statusCode": 401, + "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, + "statusCode": 406, + "name": "INVALID_PAGE_QUERY", + "message": "page 또는 pageSize 값이 잘못되었습니다. 자연수값이어야 합니다." + }, + + "TODO_INVALID": { + "errorCode": 4061, + "statusCode": 406, + "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, + "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/apiKudogExceptionResponse.decorator.ts b/src/common/decorators/apiKudogExceptionResponse.decorator.ts new file mode 100644 index 0000000..ba31ade --- /dev/null +++ b/src/common/decorators/apiKudogExceptionResponse.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..b775442 --- /dev/null +++ b/src/common/decorators/docs/auth.decorator.ts @@ -0,0 +1,151 @@ +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'; +import { TokenResponseDto } from '@/domain/auth/dtos/tokenResponse.dto'; +import { + ApiBody, + ApiCreatedResponse, + ApiHeader, + ApiOkResponse, + ApiOperation, +} from '@nestjs/swagger'; + +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', + ]), + ], + 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, + }), + ApiKudogExceptionResponse(['LOGIN_REQUIRED']), + ], + logout: [ + ApiOperation({ + summary: '로그아웃', + description: + 'refresh token을 삭제합니다. storage에서 두 토큰을 삭제해주세요. authorization header에 Bearer ${accessToken} 을 담아주세요.', + }), + ApiHeader({ + description: + 'Authorization header에 Bearer token 형태로 refresh token을 넣어주세요.', + name: 'authorization', + required: true, + }), + ApiKudogExceptionResponse(['JWT_TOKEN_INVALID']), + ApiOkResponse({ + description: 'logout 성공', + }), + ], + deleteUser: [ + ApiOperation({ + summary: '회원 탈퇴', + description: + '회원 탈퇴합니다. authorization header에 Bearer ${accessToken} 을 담아주세요.', + }), + ApiKudogExceptionResponse(['USER_NOT_FOUND']), + ApiOkResponse({ + description: '회원 탈퇴 성공', + }), + ], + changePwdRequest: [ + ApiOperation({ + summary: '비밀번호 변경 이메일 인증 요청', + description: '비밀번호를 변경하기 위하여 이메일 인증을 요청합니다.', + }), + ApiCreatedResponse({ + description: '이메일 전송 성공. 3분 안에 인증 코드를 입력해주세요.', + }), + ApiKudogExceptionResponse([ + 'EMAIL_NOT_FOUND', + 'TOO_MANY_REQUESTS', + '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_VALIDATION_EXPIRED', + ]), + ], +}; + +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..4a27f92 --- /dev/null +++ b/src/common/decorators/docs/category.decorator.ts @@ -0,0 +1,65 @@ +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'; +import { CategoryListResponseDto } from '@/domain/category/dtos/categoryListResponse.dto'; +import { ApiOkResponse, ApiOperation, ApiParam } from '@nestjs/swagger'; +type CategoryEndpoints = MethodNames; + +const CategoryDocsMap: Record = { + getProviders: [ + ApiOperation({ + summary: '학부 리스트 조회', + description: + 'DB의 학부 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiOkResponse({ + description: '학부 리스트', + type: [ProviderListResponseDto], + }), + ], + getCategories: [ + ApiOperation({ + summary: '학부 소속 카테고리 리스트 조회', + description: + 'DB의 학부 소속 카테고리 리스트 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiParam({ + description: '학부 id', + type: FindOneParams, + name: 'id', + }), + ApiOkResponse({ + description: '스크랩학부 소속 카테고리들', + type: [CategoryListResponseDto], + }), + ], + getBookmarkedProviders: [ + ApiOperation({ + summary: '즐겨찾는 학부 목록 조회', + description: + '유저의 즐겨찾는 학과 목록 조회. Authoization 헤더에 Bearer ${accessToken}을 넣어주세요', + }), + ApiOkResponse({ + description: '사용자의 즐겨찾는 학과 목록', + type: [ProviderListResponseDto], + }), + ], +}; + +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..ad1c08f 100644 --- a/src/decorators/docs/common.decorator.ts +++ b/src/common/decorators/docs/common.decorator.ts @@ -1,12 +1,7 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; +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'; export function ApiPagination() { return applyDecorators( ApiQuery({ @@ -28,9 +23,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..344c5f2 --- /dev/null +++ b/src/common/decorators/docs/index.ts @@ -0,0 +1,9 @@ +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'; +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..547e222 --- /dev/null +++ b/src/common/decorators/docs/mail.decorator.ts @@ -0,0 +1,45 @@ +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'; +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..2b56886 --- /dev/null +++ b/src/common/decorators/docs/notice.decorator.ts @@ -0,0 +1,136 @@ +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, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +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..504c388 --- /dev/null +++ b/src/common/decorators/docs/notification.decorator.ts @@ -0,0 +1,99 @@ +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, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiQuery, +} from '@nestjs/swagger'; +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..2879593 --- /dev/null +++ b/src/common/decorators/docs/scrap.decorator.ts @@ -0,0 +1,119 @@ +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, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, +} from '@nestjs/swagger'; +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..9bfe9b9 --- /dev/null +++ b/src/common/decorators/docs/subscribe.decorator.ts @@ -0,0 +1,152 @@ +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, + ApiCreatedResponse, + ApiOkResponse, + ApiOperation, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +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..e0adfda --- /dev/null +++ b/src/common/decorators/docs/user.decorator.ts @@ -0,0 +1,43 @@ +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'; + +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/common/decorators/index.ts b/src/common/decorators/index.ts new file mode 100644 index 0000000..e323918 --- /dev/null +++ b/src/common/decorators/index.ts @@ -0,0 +1,4 @@ +export * from './injectUser.decorator'; +export * from './usePagination.decorator'; +export * from './namedController'; +export * from './apiKudogExceptionResponse.decorator'; diff --git a/src/common/decorators/injectUser.decorator.ts b/src/common/decorators/injectUser.decorator.ts new file mode 100644 index 0000000..94a8825 --- /dev/null +++ b/src/common/decorators/injectUser.decorator.ts @@ -0,0 +1,16 @@ +import { JwtPayload } from '@/common/types/auth'; +import { ExecutionContext, createParamDecorator } from '@nestjs/common'; + +export const InjectUser = createParamDecorator( + (_: unknown, cts: ExecutionContext): JwtPayload => { + const request = cts.switchToHttp().getRequest(); + return request.user; + }, +); + +export const InjectToken = createParamDecorator( + (_: unknown, cts: ExecutionContext): JwtPayload => { + const request = cts.switchToHttp().getRequest(); + return request.headers.authorization.split('Bearer ')[1]; + }, +); diff --git a/src/common/decorators/namedController.ts b/src/common/decorators/namedController.ts new file mode 100644 index 0000000..f5e9e4e --- /dev/null +++ b/src/common/decorators/namedController.ts @@ -0,0 +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), + ApiKudogExceptionResponse([ + 'JWT_TOKEN_EXPIRED', + 'JWT_TOKEN_INVALID', + 'INTERNAL_SERVER_ERROR', + ]), + ); +} diff --git a/src/decorators/usePagination.decorator.ts b/src/common/decorators/usePagination.decorator.ts similarity index 67% rename from src/decorators/usePagination.decorator.ts rename to src/common/decorators/usePagination.decorator.ts index 0647e77..4c95d7d 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,10 +14,15 @@ 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) + 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 new file mode 100644 index 0000000..12938cb --- /dev/null +++ b/src/common/decorators/useValidation.ts @@ -0,0 +1,22 @@ +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, + //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); + }, + }), + ), + ApiKudogExceptionResponse(exceptions), + ); +} 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/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/common/types/auth.ts b/src/common/types/auth.ts new file mode 100644 index 0000000..2eacebc --- /dev/null +++ b/src/common/types/auth.ts @@ -0,0 +1,6 @@ +export type User = { + id: number; + name: 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/types/method.ts b/src/common/types/method.ts new file mode 100644 index 0000000..40bcf3d --- /dev/null +++ b/src/common/types/method.ts @@ -0,0 +1,3 @@ +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..dcc61a9 --- /dev/null +++ b/src/common/utils/exception.ts @@ -0,0 +1,28 @@ +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): never { + 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/decorators/index.ts b/src/decorators/index.ts deleted file mode 100644 index 36e8c8d..0000000 --- a/src/decorators/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './injectAccessUser.decorator'; -export * from './usePagination.decorator'; -export * from './injectLocalUser.decorator'; -export * from './injectRefreshUser.decorator'; diff --git a/src/decorators/injectAccessUser.decorator.ts b/src/decorators/injectAccessUser.decorator.ts deleted file mode 100644 index b473796..0000000 --- a/src/decorators/injectAccessUser.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -import { JwtPayload } from 'src/interfaces/auth'; - -export const InjectAccessUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): JwtPayload => { - const request = cts.switchToHttp().getRequest(); - return request.user; - }, -); diff --git a/src/decorators/injectLocalUser.decorator.ts b/src/decorators/injectLocalUser.decorator.ts deleted file mode 100644 index c6b46ae..0000000 --- a/src/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/decorators/injectRefreshUser.decorator.ts b/src/decorators/injectRefreshUser.decorator.ts deleted file mode 100644 index 60ec28b..0000000 --- a/src/decorators/injectRefreshUser.decorator.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ExecutionContext, createParamDecorator } from '@nestjs/common'; -import { RefreshTokenPayload } from 'src/interfaces/auth'; - -export const InjectRefreshUser = createParamDecorator( - (_: unknown, cts: ExecutionContext): RefreshTokenPayload => { - const request = cts.switchToHttp().getRequest(); - return request.user; - }, -); diff --git a/src/domain/auth/auth.controller.ts b/src/domain/auth/auth.controller.ts index 4fd74f6..4964dca 100644 --- a/src/domain/auth/auth.controller.ts +++ b/src/domain/auth/auth.controller.ts @@ -1,83 +1,76 @@ -import { Body, Controller, Delete, Post, Put, UseGuards } from '@nestjs/common'; +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, 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 type { LoginRequestDto } from './dtos/loginRequestDto'; +import { SignupRequestDto } from './dtos/signupRequest.dto'; +import { TokenResponseDto } from './dtos/tokenResponse.dto'; +import { UseJwtGuard } from './guards/jwt.guard'; -@Controller('auth') -@ApiTags('auth') +@AuthDocs +@NamedController('auth') export class AuthController { constructor(private readonly authService: AuthService) {} - @UseGuards(AuthGuard('local')) + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID', 'PASSWORD_NOT_VALID']) @Post('/login') - @Docs('login') - async login(@injectLocalUser() user: number): Promise { - return await this.authService.getToken(user); + async login(@Body() body: LoginRequestDto): Promise { + return this.authService.login(body); } + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID', 'PASSWORD_NOT_VALID']) @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.signup(body); } - @UseGuards(AuthGuard('jwt-refresh')) + @UseJwtGuard() @Post('/refresh') - @Docs('refresh') async refresh( - @InjectRefreshUser() user: RefreshTokenPayload, + @InjectUser() user: JwtPayload, + @InjectToken() token: string, ): Promise { - return await this.authService.refreshJWT(user); + return this.authService.refreshJWT(user, token); } - @UseGuards(AuthGuard('jwt-refresh')) + @UseJwtGuard() @Delete('/logout') - @Docs('logout') - async logout(@InjectRefreshUser() user: RefreshTokenPayload): Promise { - await this.authService.logout(user); + async logout(@InjectToken() token: string): Promise { + return this.authService.logout(token); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Delete('/user-info') - @Docs('deleteUser') - async deleteUser(@InjectAccessUser() user: JwtPayload): Promise { - await this.authService.deleteUser(user.id); + async deleteUser(@InjectUser() user: JwtPayload): Promise { + return this.authService.deleteUser(user.id); } + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID']) @Post('/change-password/request') - @Docs('changePwdRequest') async changePwdRequest( @Body() body: ChangePasswordRequestDto, ): Promise { - await this.authService.changePwdRequest(body); + return this.authService.changePwdRequest(body); } + @UseValidation(['NOT_ACCEPTABLE']) @Post('/change-password/verify') - @Docs('verifyChangePwdCode') async verifyChangePwdCode( @Body() body: VerifyChangePasswordRequestDto, ): Promise { - await this.authService.verifyChangePwdCode(body); + return this.authService.verifyChangePwdCode(body); } + @UseValidation(['NOT_ACCEPTABLE', 'EMAIL_NOT_VALID', 'PASSWORD_NOT_VALID']) @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..41edee0 100644 --- a/src/domain/auth/auth.module.ts +++ b/src/domain/auth/auth.module.ts @@ -1,33 +1,28 @@ +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 { - KudogUser, - ChangePwdAuthenticationEntity, - EmailAuthenticationEntity, - RefreshTokenEntity, -} from 'src/entities'; import { AuthController } from './auth.controller'; +import { AuthRepository } from './auth.repository'; 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 { ChangePwdAuthenticationEntity } from './entities/changePwd.entity'; +import { EmailAuthenticationEntity } from './entities/emailAuthentication.entity'; +import { RefreshTokenEntity } from './entities/refreshToken.entity'; @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..e9f018e --- /dev/null +++ b/src/domain/auth/auth.repository.ts @@ -0,0 +1,90 @@ +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; + 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 6d72cff..f19c8b7 100644 --- a/src/domain/auth/auth.service.ts +++ b/src/domain/auth/auth.service.ts @@ -1,115 +1,79 @@ -import { - BadRequestException, - HttpException, - HttpStatus, - Injectable, - NotFoundException, - RequestTimeoutException, - UnauthorizedException, -} from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { compare, hash } from 'bcrypt'; -import { - KudogUser, - ChangePwdAuthenticationEntity, - EmailAuthenticationEntity, - 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 { 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 { ChannelService } from 'src/channel/channel.service'; +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { compare, hash } from 'bcrypt'; +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,86 +81,52 @@ 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() .padStart(6, '0'); @@ -208,72 +138,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/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/auth/entities/changePwd.entity.ts b/src/domain/auth/entities/changePwd.entity.ts new file mode 100644 index 0000000..25df8df --- /dev/null +++ b/src/domain/auth/entities/changePwd.entity.ts @@ -0,0 +1,38 @@ +import { KudogUserEntity } from '@/domain/users/entities/kudogUser.entity'; +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + RelationId, +} from 'typeorm'; + +@Entity('change_pwd_authentication') +export class ChangePwdAuthenticationEntity { + @PrimaryGeneratedColumn() + id!: number; + + @ManyToOne( + () => KudogUserEntity, + () => undefined, + { onDelete: 'CASCADE' }, + ) + @JoinColumn({ name: 'userId' }) + user!: KudogUserEntity; + @Column() + @RelationId((entity: ChangePwdAuthenticationEntity) => entity.user) + userId!: number; + + @CreateDateColumn() + createdAt!: Date; + + @Column({ default: false }) + authenticated!: boolean; + + @Index() + @Column() + code!: string; +} diff --git a/src/entities/emailAuthentication.entity.ts b/src/domain/auth/entities/emailAuthentication.entity.ts similarity index 66% rename from src/entities/emailAuthentication.entity.ts rename to src/domain/auth/entities/emailAuthentication.entity.ts index 63721fb..ad6b1c1 100644 --- a/src/entities/emailAuthentication.entity.ts +++ b/src/domain/auth/entities/emailAuthentication.entity.ts @@ -2,26 +2,26 @@ 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; + @Index() @Column() - code: string; + code!: string; } diff --git a/src/entities/refreshToken.entity.ts b/src/domain/auth/entities/refreshToken.entity.ts similarity index 60% rename from src/entities/refreshToken.entity.ts rename to src/domain/auth/entities/refreshToken.entity.ts index 62c4fba..b00089a 100644 --- a/src/entities/refreshToken.entity.ts +++ b/src/domain/auth/entities/refreshToken.entity.ts @@ -1,31 +1,37 @@ -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from '@/domain/users/entities/kudogUser.entity'; 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, + () => KudogUserEntity, (user) => user.refreshTokens, { onDelete: 'CASCADE', }, ) @JoinColumn({ name: 'userId' }) - user: KudogUser; - + user!: KudogUserEntity; + @Column() @RelationId((refreshToken: RefreshTokenEntity) => refreshToken.user) - userId: number; + userId!: number; + @Index() @Column() - token: string; + 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..ee789f9 --- /dev/null +++ b/src/domain/auth/guards/jwt.guard.ts @@ -0,0 +1,30 @@ +import { ApiKudogExceptionResponse } from '@/common/decorators'; +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 12e2bb4..0000000 --- a/src/domain/auth/passport/accessToken.strategy.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { Strategy, ExtractJwt } from 'passport-jwt'; -import { JwtPayload } from 'src/interfaces/auth'; - -@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; - } -} diff --git a/src/domain/auth/passport/local.strategy.ts b/src/domain/auth/passport/local.strategy.ts deleted file mode 100644 index 2e5c3f9..0000000 --- a/src/domain/auth/passport/local.strategy.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Strategy } from 'passport-local'; -import { PassportStrategy } from '@nestjs/passport'; -import { Injectable } from '@nestjs/common'; -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); - } -} diff --git a/src/domain/auth/passport/refreshToken.strategy.ts b/src/domain/auth/passport/refreshToken.strategy.ts deleted file mode 100644 index befad8a..0000000 --- a/src/domain/auth/passport/refreshToken.strategy.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { PassportStrategy } from '@nestjs/passport'; -import { Request } from 'express'; -import { Strategy, ExtractJwt } from 'passport-jwt'; -import { JwtPayload, RefreshTokenPayload } from 'src/interfaces/auth'; - -@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, - }; - } -} diff --git a/src/domain/category/category.controller.ts b/src/domain/category/category.controller.ts index 9ee4d37..842fe9d 100644 --- a/src/domain/category/category.controller.ts +++ b/src/domain/category/category.controller.ts @@ -1,39 +1,39 @@ -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { InjectUser, NamedController } from '@/common/decorators'; +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 { Body, Get, Param, UsePipes, ValidationPipe } from '@nestjs/common'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; import { CategoryService } from './category.service'; -import { AuthGuard } from '@nestjs/passport'; -import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; -import { InjectAccessUser } from 'src/decorators'; -import { JwtPayload } from 'src/interfaces/auth'; 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') + @UseJwtGuard() @Get('/providers') async getProviders(): Promise { return this.categoryService.getProviders(); } - @UseGuards(AuthGuard('jwt-access')) - @Docs('getCategories') + @UseJwtGuard() + @UseValidation(['NOT_ACCEPTABLE']) @Get('/by-providers/:id') async getCategories( - @Param('id', IntValidationPipe) id: number, + @Param() params: FindOneParams, ): Promise { + const { id } = params; return this.categoryService.getCategories(id); } - @UseGuards(AuthGuard('jwt-access')) - @Docs('getBookmarkedProviders') + @UseJwtGuard() @Get('/providers/bookmarks') async getBookmarkedProviders( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { return this.categoryService.getBookmarkedProviders(user.id); } diff --git a/src/domain/category/category.module.ts b/src/domain/category/category.module.ts index 763c2d7..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 { CategoryService } from './category.service'; 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/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/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 deafbce..3f5fe30 100644 --- a/src/entities/category.entity.ts +++ b/src/domain/category/entities/category.entity.ts @@ -1,14 +1,15 @@ +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 { Notice } from 'src/entities'; -import { ProviderEntity } from 'src/entities'; -import { CategoryMap } from 'src/enums'; -import { CategoryPerSubscribeBoxEntity } from './categoryPerSubscribes.entity'; +import { ProviderEntity } from './provider.entity'; @Entity('category') export class CategoryEntity { @@ -21,15 +22,17 @@ export class CategoryEntity { @Column() url: string; - @Column({ type: 'enum', enum: CategoryMap, default: CategoryMap.공지사항 }) - mappedCategory: CategoryMap; - @ManyToOne( () => 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 2e1d249..93b4945 100644 --- a/src/entities/provider.entity.ts +++ b/src/domain/category/entities/provider.entity.ts @@ -1,6 +1,6 @@ import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; -import { CategoryEntity } from 'src/entities'; -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 98a13d3..064460c 100644 --- a/src/entities/providerBookmark.entity.ts +++ b/src/domain/category/entities/providerBookmark.entity.ts @@ -1,8 +1,9 @@ +import { KudogUserEntity } from '@/domain/users/entities/kudogUser.entity'; import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { ProviderEntity, KudogUser } from 'src/entities'; +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/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..e7e9f44 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'; +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.module.ts b/src/domain/mail/mail.module.ts index b518193..1bae544 100644 --- a/src/domain/mail/mail.module.ts +++ b/src/domain/mail/mail.module.ts @@ -1,20 +1,20 @@ +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, + KudogUserEntity, Notice, SubscribeBoxEntity, } from 'src/entities'; +import { MailController } from './mail.controller'; +import { MailService } from './mail.service'; @Module({ 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 08981d7..a86add7 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,17 +7,15 @@ import { NotFoundException, RequestTimeoutException, } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; import { InjectRepository } from '@nestjs/typeorm'; import { EmailAuthenticationEntity, - KudogUser, + KudogUserEntity, Notice, 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 'src/utils/date'; @Injectable() export class MailService { @@ -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/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/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 index 9076f56..d71498e 100644 --- a/src/entities/notice.entity.ts +++ b/src/domain/notice/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/domain/notice/notice.controller.ts b/src/domain/notice/notice.controller.ts index ec59dbe..2ca7b32 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 { InjectUser, 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 { UseJwtGuard } from '../auth/guards/jwt.guard'; +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')) + @UseJwtGuard() @Get('/list') - @Docs('getNoticeList') async getNoticeList( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Put('/:noticeId/scrap/:scrapBoxId') - @Docs('scrapNotice') async scrapNotice( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Get('/info/:id') - @Docs('getNoticeInfoById') async getNoticeInfoById( @Param('id', IntValidationPipe) id: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { - return await this.noticeService.getNoticeInfoById(id, user.id); + return this.noticeService.getNoticeInfoById(id, user.id); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Post('/add-request') - @Docs('addNoticeRequest') async addNoticeRequest( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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..339c00a 100644 --- a/src/domain/notification/notification.controller.ts +++ b/src/domain/notification/notification.controller.ts @@ -1,86 +1,68 @@ -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 { 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 { UseJwtGuard } from '../auth/guards/jwt.guard'; 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')) + @UseJwtGuard() @Get('') - @Docs('getNotifications') async getNotifications( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Get('/new') - @Docs('getNewNotifications') async getNewNotifications( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @UsePagination() pageQuery: PageQuery, ): Promise> { - return await this.notificationService.getNewNotifications( - user.id, - pageQuery, - ); + return this.notificationService.getNewNotifications(user.id, pageQuery); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Post('/token') - @Docs('registerToken') async registerToken( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Delete('/token') - @Docs('deleteToken') async deleteToken( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Get('/status') - @Docs('getTokenStatus') async getTokenStatus( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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만 보내주면, 해당 유저가 등록한 기기에 모두 알림을 보냅니다', - }) + @UseJwtGuard() @Get('/test') - async sendNotification(@InjectAccessUser() user: JwtPayload): Promise { - return await 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/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/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.controller.ts b/src/domain/scrap/scrap.controller.ts index 6098d5f..e15008d 100644 --- a/src/domain/scrap/scrap.controller.ts +++ b/src/domain/scrap/scrap.controller.ts @@ -1,79 +1,66 @@ -import { - Body, - Controller, - Get, - Post, - Param, - Put, - Delete, - UseGuards, -} from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; -import { AuthGuard } from '@nestjs/passport'; -import { ScrapService } from './scrap.service'; +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, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { IntValidationPipe } from 'src/pipes/intValidation.pipe'; +import { UseJwtGuard } from '../auth/guards/jwt.guard'; 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')) + @UseJwtGuard() @Post('/box') - @Docs('createScrapBox') async createScrapBox( @Body() body: ScrapBoxRequestDto, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { - return await this.scrapService.createScrapBox(user.id, body); + return this.scrapService.createScrapBox(user.id, body); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Get('/box/:scrapBoxId') - @Docs('getScrapBoxInfo') async getScrapBoxInfo( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { - return await this.scrapService.getScrapBoxInfo(user.id, scrapBoxId); + return this.scrapService.getScrapBoxInfo(user.id, scrapBoxId); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Get('/box') - @Docs('getScrapBoxes') async getScrapBoxes( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Put('/box/:scrapBoxId') - @Docs('updateScrapBox') async updateScrapBox( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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')) + @UseJwtGuard() @Delete('/box/:scrapBoxId') - @Docs('deleteScrapBox') async deleteScrapBox( @Param('scrapBoxId', IntValidationPipe) scrapBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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.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/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/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/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 index 7268641..b6955ea 100644 --- a/src/entities/categoryPerSubscribes.entity.ts +++ b/src/domain/subscribe/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/domain/subscribe/subscribe.controller.ts b/src/domain/subscribe/subscribe.controller.ts index e6cb103..7a60c86 100644 --- a/src/domain/subscribe/subscribe.controller.ts +++ b/src/domain/subscribe/subscribe.controller.ts @@ -1,106 +1,86 @@ -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 { 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 { IntValidationPipe } from 'src/pipes/intValidation.pipe'; +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'; +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')) + @UseJwtGuard() @Post('/box') - @Docs('createSubscribeBox') async createSubscribeBox( @Body() body: SubscribeBoxRequestDto, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { - return await this.subscribeService.createSubscribeBox(user.id, body); + return this.subscribeService.createSubscribeBox(user.id, body); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Get('/box/:subscribeBoxId') - @Docs('getSubscribeBoxInfo') async getSubscribeInfo( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Query('date') date: string, ): Promise { - return await this.subscribeService.getSubscribeBoxInfo( + return this.subscribeService.getSubscribeBoxInfo( user.id, subscribeBoxId, date, ); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Get('/box') - @Docs('getSubscribeBoxes') - async getSubscribees( - @InjectAccessUser() user: JwtPayload, + async getSubscribeBoxes( + @InjectUser() 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')) + @UseJwtGuard() @Put('/box/:subscribeBoxId') - @Docs('updateSubscribeBox') - async updateSubscribe( + async updateSubscribeBox( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, @Body() body: SubscribeBoxRequestDto, ): Promise { - return await this.subscribeService.updateSubscribeBox( + return this.subscribeService.updateSubscribeBox( subscribeBoxId, user.id, body, ); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Delete('/box/:subscribeBoxId') - @Docs('deleteSubscribeBox') - async deleteSubscribe( + async deleteSubscribeBox( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { - return await this.subscribeService.deleteSubscribeBox( - subscribeBoxId, - user.id, - ); + return this.subscribeService.deleteSubscribeBox(subscribeBoxId, user.id); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Get('/box/:subscribeBoxId/notices') - @Docs('getNoticesByBoxWithDate') async getNoticesByBoxWithDate( @Param('subscribeBoxId', IntValidationPipe) subscribeBoxId: number, - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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.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/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/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 73% rename from src/entities/kudogUser.entity.ts rename to src/domain/users/entities/kudogUser.entity.ts index 71a9bf6..829fda0 100644 --- a/src/entities/kudogUser.entity.ts +++ b/src/domain/users/entities/kudogUser.entity.ts @@ -1,18 +1,25 @@ -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; import { - ScrapBoxEntity, - SubscribeBoxEntity, NotificationEntity, NotificationTokenEntity, RefreshTokenEntity, + ScrapBoxEntity, + SubscribeBoxEntity, } from 'src/entities'; -import { ProviderBookmark } from './providerBookmark.entity'; +import { + Column, + Entity, + Index, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { ProviderBookmarkEntity } from '../../category/entities/providerBookmark.entity'; @Entity('kudog_user') -export class KudogUser { +export class KudogUserEntity { @PrimaryGeneratedColumn() id: number; + @Index() @Column() email: string; @@ -53,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 new file mode 100644 index 0000000..20b1577 --- /dev/null +++ b/src/domain/users/user.repository.ts @@ -0,0 +1,46 @@ +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; + + constructor(@InjectDataSource() private dataSource: DataSource) { + this.entityRepository = this.dataSource.getRepository(KudogUserEntity); + } + + 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: KudogUserEntity): 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: KudogUserEntity, + passwordHash: string, + ): Promise { + entity.passwordHash = passwordHash; + await this.entityRepository.save(entity); + } +} diff --git a/src/domain/users/users.controller.ts b/src/domain/users/users.controller.ts index 424c55b..cd80f7d 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 { 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 { UseJwtGuard } from '../auth/guards/jwt.guard'; 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')) + @UseJwtGuard() @Get('/info') - @Docs('getUserInfo') async getUserInfo( - @InjectAccessUser() user: JwtPayload, + @InjectUser() user: JwtPayload, ): Promise { - return await this.userService.getUserInfo(user.id); + return this.userService.getUserInfo(user.id); } - @UseGuards(AuthGuard('jwt-access')) + @UseJwtGuard() @Put('/info') - @Docs('modifyUserInfo') async modifyUserInfo( - @InjectAccessUser() user: JwtPayload, + @InjectUser() 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/domain/users/users.module.ts b/src/domain/users/users.module.ts index f3a6d79..a40888e 100644 --- a/src/domain/users/users.module.ts +++ b/src/domain/users/users.module.ts @@ -1,12 +1,16 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { KudogUserEntity, ProviderBookmarkEntity } from 'src/entities'; +import { UserRepository } from './user.repository'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { KudogUser, ProviderBookmark } from 'src/entities'; @Module({ controllers: [UsersController], - providers: [UsersService], - imports: [TypeOrmModule.forFeature([KudogUser, ProviderBookmark])], + providers: [UsersService, UserRepository], + exports: [UserRepository], + 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 09b0571..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 { KudogUser, ProviderBookmark } from 'src/entities'; +import { hash } from 'bcrypt'; +import { KudogUserEntity, ProviderBookmarkEntity } from 'src/entities'; import { Repository } from 'typeorm'; import { ModifyInfoRequestDto, UserInfoResponseDto } from './dtos/userInfo.dto'; -import { hash } from 'bcrypt'; @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/changePwd.entity.ts b/src/entities/changePwd.entity.ts deleted file mode 100644 index 56d9364..0000000 --- a/src/entities/changePwd.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - JoinColumn, - OneToOne, - PrimaryGeneratedColumn, -} from 'typeorm'; -import { KudogUser } from './kudogUser.entity'; - -@Entity('change_pwd_authentication') -export class ChangePwdAuthenticationEntity { - @PrimaryGeneratedColumn() - id: number; - - @OneToOne( - () => KudogUser, - () => undefined, - { onDelete: 'CASCADE' }, - ) - @JoinColumn() - user: KudogUser; - - @CreateDateColumn() - createdAt: Date; - - @Column({ nullable: true }) - expireAt?: Date; - - @Column({ default: false }) - authenticated: boolean; - - @Column() - code: string; -} diff --git a/src/entities/index.ts b/src/entities/index.ts index f75fe72..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 './emailAuthentication.entity'; -export * from './notice.entity'; -export * from './provider.entity'; -export * from './changePwd.entity'; +export * from '../domain/category/entities/category.entity'; +export * from '../domain/users/entities/kudogUser.entity'; +export * from '../domain/auth/entities/emailAuthentication.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 './refreshToken.entity'; -export * from './providerBookmark.entity'; +export * from '../domain/auth/entities/refreshToken.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 b728da4..7393575 100644 --- a/src/entities/notificationToken.entity.ts +++ b/src/entities/notificationToken.entity.ts @@ -1,11 +1,11 @@ -import { KudogUser } from 'src/entities'; +import { KudogUserEntity } from 'src/entities'; import { - Entity, Column, + Entity, + JoinColumn, ManyToOne, PrimaryGeneratedColumn, RelationId, - JoinColumn, } from 'typeorm'; @Entity('notification_token') @@ -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 3901c94..f86cc83 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') @@ -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 e99236d..1ba6d7e 100644 --- a/src/entities/scrapBox.entity.ts +++ b/src/entities/scrapBox.entity.ts @@ -1,3 +1,4 @@ +import { KudogUserEntity, 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 { @@ -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 a8b08d7..f33cdfe 100644 --- a/src/entities/subscribeBox.entity.ts +++ b/src/entities/subscribeBox.entity.ts @@ -1,3 +1,4 @@ +import { KudogUserEntity } from 'src/entities'; import { Column, Entity, @@ -7,8 +8,7 @@ import { PrimaryGeneratedColumn, RelationId, } from 'typeorm'; -import { KudogUser } from 'src/entities'; -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/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..52d6bb8 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, + KudogUserEntity, 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], @@ -18,7 +18,7 @@ import { NotificationModule } from 'src/domain/notification/notification.module' Notice, ProviderEntity, CategoryEntity, - KudogUser, + KudogUserEntity, ]), MailModule, NotificationModule, 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/auth.ts b/src/interfaces/auth.ts deleted file mode 100644 index 7b82735..0000000 --- a/src/interfaces/auth.ts +++ /dev/null @@ -1,12 +0,0 @@ -export interface User { - id: number; - name: string; -} - -export interface JwtPayload extends User { - signedAt: string; -} - -export interface RefreshTokenPayload extends JwtPayload { - refreshToken: string; -} 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 9ba3bf1..1f65f82 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,54 +1,43 @@ +import { ExceptionFilter } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { AppModule } from './app.module'; +import { ChannelService } from './domain/channel/channel.service'; import { HttpExceptionFilter } from './filters/httpException.filter'; -import { ChannelService } from './channel/channel.service'; 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()); - + const logger = app.get(Logger); + app.useLogger(logger); app.useGlobalFilters(...filters); - 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/src/middlewares/auth.middleware.spec.ts b/src/middlewares/auth.middleware.spec.ts new file mode 100644 index 0000000..23e4884 --- /dev/null +++ b/src/middlewares/auth.middleware.spec.ts @@ -0,0 +1,80 @@ +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', + }; + 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/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() diff --git a/yarn.lock b/yarn.lock index e942674..7be8a9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1343,16 +1343,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" @@ -1397,7 +1387,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: @@ -1787,7 +1777,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: @@ -1866,7 +1856,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: @@ -1937,36 +1927,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" @@ -3275,6 +3235,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": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" @@ -3541,6 +3508,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" @@ -3933,7 +3907,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: @@ -4265,6 +4239,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" @@ -4930,6 +4911,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" @@ -6004,6 +5992,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" @@ -6147,7 +6142,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: @@ -6264,20 +6259,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" @@ -6287,14 +6281,14 @@ __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" + 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" @@ -7641,7 +7635,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: @@ -7866,43 +7860,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" @@ -7969,13 +7926,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" @@ -8085,7 +8035,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: @@ -8095,6 +8045,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" @@ -8102,7 +8088,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: @@ -8238,6 +8224,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" @@ -8461,6 +8454,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" @@ -8844,6 +8847,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" @@ -9953,7 +9963,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