diff --git a/docker/Dockerfile.server b/docker/Dockerfile.server index 5454137b..4905cc41 100644 --- a/docker/Dockerfile.server +++ b/docker/Dockerfile.server @@ -3,15 +3,19 @@ FROM node:18 AS build RUN mkdir -p /var/www/otlplus-server WORKDIR /var/www/otlplus-server -COPY package*.json ./ +COPY package.json ./ RUN npm install COPY . . RUN npm run client:generate RUN npm run build -FROM node:18-slim AS server -RUN apt-get update -y && apt-get install -y openssl +FROM node:18 AS server +RUN rm -rf /var/lib/apt/lists/* +RUN apt-get clean +RUN apt-get install -y openssl + +# RUN apt-get update -y && apt-get install -y openssl COPY --from=build /var/www/otlplus-server/ /var/www/otlplus-server/ diff --git a/package-lock.json b/package-lock.json index 6e33741f..00974ebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,10 @@ "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "uuid": "^10.0.0", + "winston": "^3.14.2", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -55,6 +58,7 @@ "@types/jest": "28.1.4", "@types/node": "^16.18.23", "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "cross-env": "^7.0.3", @@ -784,6 +788,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -2350,6 +2364,17 @@ "@types/node": "*" } }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "node_modules/@types/validator": { "version": "13.12.0", "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz", @@ -2972,6 +2997,11 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3719,6 +3749,37 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/colorspace/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/colorspace/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/colorspace/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4162,6 +4223,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -4795,6 +4861,11 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -4831,6 +4902,14 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -4909,6 +4988,11 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", @@ -5667,7 +5751,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -6519,6 +6602,11 @@ "node": ">=6" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -7110,6 +7198,30 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/logform": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.1.tgz", + "integrity": "sha512-CdaO738xRapbKIMVn2m4F6KTj4j7ooJ8POVnebSgKo3KBz5axNXRAL7ZdRjIV6NOr2Uf4vjtRkxrFETOioCqSA==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7594,6 +7706,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -7632,6 +7752,14 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -8431,6 +8559,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -8801,6 +8937,14 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -9188,6 +9332,11 @@ "node": ">=8" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -9261,6 +9410,14 @@ "tree-kill": "cli.js" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-jest": { "version": "28.0.8", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.8.tgz", @@ -9604,6 +9761,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/uuid-random": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/uuid-random/-/uuid-random-1.3.2.tgz", @@ -9846,6 +10015,91 @@ "node": ">=8.12.0" } }, + "node_modules/winston": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.14.2.tgz", + "integrity": "sha512-CO8cdpBB2yqzEf8v895L+GNKYJiEq8eKlHU38af3snQBQ+sdAIUepjMSguOIJC7ICbzm0ZI+Af2If4vIJrtmOg==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.6.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, + "node_modules/winston-transport": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.1.tgz", + "integrity": "sha512-wQCXXVgfv/wUPOfb2x0ruxzwkcZfxcktz6JIMUaPLmcNhO4bZTwA/WtDWK74xV3F2dKu8YadrFv0qhwYjVEwhA==", + "dependencies": { + "logform": "^2.6.1", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index 64b7c94b..4fc57591 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,10 @@ "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "uuid": "^10.0.0", + "winston": "^3.14.2", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -88,6 +91,7 @@ "@types/jest": "28.1.4", "@types/node": "^16.18.23", "@types/supertest": "^6.0.2", + "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "cross-env": "^7.0.3", diff --git a/src/app.module.ts b/src/app.module.ts index 7b916df9..c3a6e51b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { APP_GUARD } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AppController } from './app.controller'; @@ -29,6 +29,7 @@ import { ClsModule } from 'nestjs-cls'; import { ClsPluginTransactional } from '@nestjs-cls/transactional'; import { PrismaService } from '@src/prisma/prisma.service'; import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma'; +// import { LoggingMiddleware } from "@src/common/middleware/http.logging.middleware"; @Module({ imports: [ diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts index aa2454f5..5370b02f 100644 --- a/src/bootstrap/bootstrap.ts +++ b/src/bootstrap/bootstrap.ts @@ -5,6 +5,8 @@ import session from 'express-session'; import { AppModule } from '../app.module'; import settings from '../settings'; import morgan = require('morgan'); +import { LoggingInterceptor } from '@src/common/middleware/http.logging.middleware'; +import { HttpExceptionFilter } from '@src/common/filter/http.exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -53,8 +55,10 @@ async function bootstrap() { }), ); + app.useGlobalInterceptors(new LoggingInterceptor()); + app.useGlobalFilters(new HttpExceptionFilter()); app.enableShutdownHooks(); - return app.listen(8000); + return app.listen(8080); } bootstrap() diff --git a/src/common/filter/http.exception.filter.ts b/src/common/filter/http.exception.filter.ts new file mode 100644 index 00000000..00f07e0a --- /dev/null +++ b/src/common/filter/http.exception.filter.ts @@ -0,0 +1,93 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import * as winston from 'winston'; +import 'winston-daily-rotate-file'; +import { v4 as uuidv4 } from 'uuid'; + +// JSON 포맷을 커스터마이징하여 메시지와 속성 순서를 설정 +const customFormat = winston.format.printf(({ message }) => { + return JSON.stringify(message); +}); + +// 파일 핸들러 설정 +const fileTransport = new winston.transports.DailyRotateFile({ + filename: 'logs/response-%DATE%.log', // 파일 이름 패턴 + datePattern: 'YYYY-MM-DD', // 날짜 패턴 + zippedArchive: true, // 압축 여부 + maxSize: '10m', // 파일 최대 크기 + maxFiles: '10d', // 백업 파일 수 + handleExceptions: true, // 예외 처리 + format: winston.format.combine(customFormat), +}); + +const logger = winston.createLogger({ + transports: [fileTransport], + exitOnError: false, // 예외 발생 시 프로세스 종료하지 않음 +}); + +@Catch(HttpException) +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: HttpException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const startTime = Date.now(); + const requestId = request.headers['uuid']; + + const { method, headers, query } = request; + + // 요청 데이터 처리 + let requestData = {}; + try { + if (method === 'GET') { + requestData = { ...query }; + } else if (method === 'POST') { + requestData = { ...request.body }; + } else { + requestData = { ...request.body }; + } + } catch (error) { + requestData = {}; // 예외 발생 시 빈 객체로 처리 + } + const requestLog = { + method, + path: request.path, // URI만 포함하도록 수정 + UUID: requestId, + data: requestData, + user: request?.user?.sid ?? '', + // meta, + }; + + // 예외 로그 기록 + const errorResponseLog = { + status: status, + data: response?.body ?? '', + }; + + // 전체 로그 객체 + const logData = { + request: requestLog, + response: errorResponseLog, + }; + + // 로그 출력 + logger.error(logData); + + // 응답 생성 + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} diff --git a/src/common/middleware/http.logging.middleware.ts b/src/common/middleware/http.logging.middleware.ts new file mode 100644 index 00000000..7dd283a5 --- /dev/null +++ b/src/common/middleware/http.logging.middleware.ts @@ -0,0 +1,146 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Observable, throwError } from 'rxjs'; +import { tap, catchError } from 'rxjs/operators'; +import { v4 as uuidv4 } from 'uuid'; +import * as winston from 'winston'; +import 'winston-daily-rotate-file'; + +// JSON 포맷을 커스터마이징하여 메시지와 속성 순서를 설정 +const customFormat = winston.format.printf(({ message }) => { + return JSON.stringify(message); +}); + +// 파일 핸들러 설정 +const fileTransport = new winston.transports.DailyRotateFile({ + filename: 'logs/response-%DATE%.log', // 파일 이름 패턴 + datePattern: 'YYYY-MM-DD', // 날짜 패턴 + zippedArchive: true, // 압축 여부 + maxSize: '10m', // 파일 최대 크기 + maxFiles: '10d', // 백업 파일 수 + handleExceptions: true, // 예외 처리 + format: winston.format.combine(customFormat), +}); + +export const logger = winston.createLogger({ + transports: [fileTransport], + exitOnError: false, // 예외 발생 시 프로세스 종료하지 않음 +}); + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req = context.switchToHttp().getRequest(); + const res = context.switchToHttp().getResponse(); + + const startTime = Date.now(); + const requestId = req.headers['uuid']; + + const { method, headers, query } = req; + + // 요청 데이터 처리 + let requestData = {}; + try { + if (method === 'GET') { + requestData = { ...query }; + } else if (method === 'POST') { + requestData = { ...req.body }; + } else { + requestData = { ...req.body }; + } + } catch (error) { + requestData = {}; // 예외 발생 시 빈 객체로 처리 + } + + const meta = { + tz: new Date().toLocaleString('en-US', { + timeZone: 'Asia/Seoul', + timeZoneName: 'short', + }), + remote_host: req.hostname, + content_length: headers['content-length'] || '', + path_info: req.path, + remote_addr: req.ip, + content_type: headers['content-type'] || '', + http_host: headers['host'] || '', + http_user_agent: headers['user-agent'] || '', + }; + + const requestLog = { + method, + timestamp: (new Date()).toISOString(), + path: req.path, // URI만 포함하도록 수정 + UUID: requestId, + data: requestData, + user: req?.user?.sid ?? '', + // meta, + }; + + return next.handle().pipe( + tap((responseBody) => { + res.on('finish', () => { + const duration = Date.now() - startTime; + const { statusCode } = res; + + const responseLog = { + status: statusCode, + // headers: { + // 'Content-Type': res.getHeader('Content-Type') || '', + // // ...res.getHeaders(), // 모든 응답 헤더를 포함 + // }, + // charset: res.charset || 'utf-8', + data: responseBody, // Response Body 저장 + }; + + // request와 response를 하나의 객체로 묶어서 로깅 + const logData = { + request: requestLog, + response: responseLog, + duration: duration, + }; + + logger.info(logData); + }); + }), + catchError((err) => { + res.on('finish', () => { + const duration = Date.now() - startTime; + const statusCode = + err instanceof HttpException + ? err.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const errorResponseLog = { + status: statusCode, + headers: { + 'Content-Type': res.getHeader('Content-Type') || '', + ...res.getHeaders(), // 모든 응답 헤더를 포함 + }, + charset: res.charset || 'utf-8', + data: { + message: err.message || 'Internal Server Error', + stack: err.stack || '', // Stack trace 포함 (원하는 경우) + }, + duration: `${duration}ms`, + }; + + // 예외 발생 시에도 request는 그대로 남기고, error response를 로깅 + const logData = { + request: requestLog, + response: errorResponseLog, + }; + + logger.error(logData); + }); + + return throwError(() => err); + }), + ); + } +} diff --git a/src/modules/auth/auth.config.ts b/src/modules/auth/auth.config.ts index ee8dcb21..46c6e6c2 100644 --- a/src/modules/auth/auth.config.ts +++ b/src/modules/auth/auth.config.ts @@ -6,6 +6,7 @@ import { JwtCommand } from './command/jwt.command'; import { SidCommand } from './command/sid.command'; import { IsPublicCommand } from './command/isPublic.command'; import { AuthChain } from './auth.chain'; +import { SessionCommand } from '@src/modules/auth/command/session.command'; @Injectable() export class AuthConfig { @@ -14,6 +15,7 @@ export class AuthConfig { private readonly jwtCommand: JwtCommand, private readonly sidCommand: SidCommand, private readonly isPublicCommand: IsPublicCommand, + private readonly sessionCommand: SessionCommand, ) {} public async config(env: string) { @@ -34,7 +36,7 @@ export class AuthConfig { return this.authChain .register(this.isPublicCommand) .register(this.sidCommand) - .register(this.jwtCommand); + .register(this.sessionCommand); }; private getProdGuardConfig = () => { diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index e65e0f3a..cf06f799 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -12,6 +12,7 @@ import { IsPublicCommand } from './command/isPublic.command'; import { JwtCommand } from './command/jwt.command'; import { SidCommand } from './command/sid.command'; import { AuthConfig } from './auth.config'; +import { SessionCommand } from '@src/modules/auth/command/session.command'; @Module({ imports: [ @@ -29,6 +30,7 @@ import { AuthConfig } from './auth.config'; IsPublicCommand, JwtCommand, SidCommand, + SessionCommand, AuthConfig, ], exports: [AuthService, AuthConfig, AuthChain], diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 1fa03a9f..95bd70eb 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -130,4 +130,8 @@ export class AuthService { ): Promise { return await this.userRepository.updateUser(userId, user); } + + async findBySessionKey(session_key: string) { + return await this.userRepository.findBySessionKey(session_key); + } } diff --git a/src/modules/auth/command/session.command.ts b/src/modules/auth/command/session.command.ts new file mode 100644 index 00000000..64614686 --- /dev/null +++ b/src/modules/auth/command/session.command.ts @@ -0,0 +1,44 @@ +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { Request } from 'express'; +import { AuthService } from '../auth.service'; +import { JwtService } from '@nestjs/jwt'; +import { AuthCommand, AuthResult } from '../auth.command'; + +@Injectable() +export class SessionCommand implements AuthCommand { + constructor( + private reflector: Reflector, + private authService: AuthService, + private jwtService: JwtService, + ) {} + + public async next( + context: ExecutionContext, + prevResult: AuthResult, + ): Promise { + const request = context.switchToHttp().getRequest(); + const response = context.switchToHttp().getResponse(); + const accessToken = this.extractTokenFromCookie(request, 'accessToken'); + const cookie = request.cookies['sessionid']; + if (!cookie) { + return prevResult; + } + const user = await this.authService.findBySessionKey(cookie); + request['user'] = user; + prevResult.authentication = true; + prevResult.authorization = true; + return prevResult; + } + + private extractTokenFromCookie( + request: Request, + type: 'accessToken' | 'refreshToken', + ): string | undefined { + const cookie = request.cookies[type]; + if (cookie) { + return cookie; + } + return undefined; + } +} diff --git a/src/modules/semesters/semesters.controller.ts b/src/modules/semesters/semesters.controller.ts index 8ddaa0ee..4e3dae94 100644 --- a/src/modules/semesters/semesters.controller.ts +++ b/src/modules/semesters/semesters.controller.ts @@ -2,12 +2,14 @@ import { Controller, Get, Query } from '@nestjs/common'; import { ISemester } from 'src/common/interfaces/ISemester'; import { toJsonSemester } from '../../common/interfaces/serializer/semester.serializer'; import { SemestersService } from './semesters.service'; +import { Public } from '@src/common/decorators/skip-auth.decorator'; @Controller('api/semesters') export class SemestersController { constructor(private readonly semestersService: SemestersService) {} @Get() + @Public() async getSemesters(@Query() query: ISemester.QueryDto) { const semesters = await this.semestersService.getSemesters(query); return semesters.map((semester) => toJsonSemester(semester)); diff --git a/src/prisma/repositories/user.repository.ts b/src/prisma/repositories/user.repository.ts index c0588783..e7030c5c 100644 --- a/src/prisma/repositories/user.repository.ts +++ b/src/prisma/repositories/user.repository.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { Prisma, session_userprofile, subject_semester } from '@prisma/client'; import { PrismaService } from '../prisma.service'; @@ -12,6 +12,54 @@ export class UserRepository { }); } + async findBySessionKey(sessionKey: string) { + const djangoSession = await this.prisma.django_session.findFirstOrThrow({ + where: { + session_key: sessionKey, + }, + }); + const decodedSessionData = Buffer.from( + djangoSession.session_data, + 'base64', + ).toString('utf8'); + const regexMatch = decodedSessionData.match(/{.*}/) + if(regexMatch === null) + throw new InternalServerErrorException("json string error") + const jsonString = regexMatch[0] + const json_obj = JSON.parse(jsonString); + const sessionData_obj = json_obj + const userId = Number(sessionData_obj['_auth_user_id']); + const user = await this.prisma.session_userprofile.findUnique({ + select: { + id: true, + user_id: true, + student_id: true, + sid: true, + language: true, + portal_check: true, + department_id: true, + email: true, + }, + where: { + user_id: userId, + }, + }); + const auth_user = await this.prisma.auth_user.findUnique({ + select: { + date_joined: true, + first_name: true, + last_name: true, + }, + where: { + id: userId, + }, + }); + return { + ...user, + ...auth_user, + }; + } + async createUser( user: Prisma.session_userprofileCreateInput, ): Promise { diff --git a/src/settings.ts b/src/settings.ts index 127e3a56..c7c8dcc3 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -58,22 +58,22 @@ const getPrismaConfig = (): Prisma.PrismaClientOptions => { }, errorFormat: 'pretty', log: [ - { - emit: 'event', - level: 'query', - }, + // { + // emit: 'event', + // level: 'query', + // }, { emit: 'stdout', level: 'error', }, - { - emit: 'stdout', - level: 'info', - }, - { - emit: 'stdout', - level: 'warn', - }, + // { + // emit: 'stdout', + // level: 'info', + // }, + // { + // emit: 'stdout', + // level: 'warn', + // }, ], }; };