Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/cicd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand Down
3 changes: 0 additions & 3 deletions biome.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"organizeImports": {
"enabled": true
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
Expand Down
11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
61 changes: 49 additions & 12 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -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: [
Expand All @@ -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: {
Expand All @@ -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,
Expand All @@ -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('*');
}
}
145 changes: 145 additions & 0 deletions src/asset/error.json
Original file line number Diff line number Diff line change
@@ -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": "이메일 전송에 실패했습니다. 잠시 후에 다시 시도해주세요."
}
}
38 changes: 38 additions & 0 deletions src/common/decorators/apiKudogExceptionResponse.decorator.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Loading