diff --git a/.dockerignore b/.dockerignore index 8ffa241b..bafcba69 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,6 @@ /node_modules +/dist +.env .env.test .env.production .env.development @@ -6,6 +8,7 @@ *.code-workspace *.swp /logs/*.log +.vscode # AdminJS 관련 디렉토리 .adminjs diff --git a/.env.example b/.env.example index ea107da9..9e2688e9 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,8 @@ CORS_WHITELIST=[CORS 정책에서 허용하는 도메인의 목록(e.g. ["http:/ GOOGLE_APPLICATION_CREDENTIALS=[GOOGLE_APPLICATION_CREDENTIALS JSON] TEST_ACCOUNTS=[스팍스SSO로 로그인시 무조건 테스트로 로그인이 가능한 허용 아이디 목록] SLACK_REPORT_WEBHOOK_URL=[Slack 웹훅 URL들이 담긴 JSON] +NAVER_MAP_API_ID=[네이버 지도 API ID] +NAVER_MAP_API_KEY=[네이버 지도 API KEY] # optional environment variables for taxiSampleGenerator SAMPLE_NUM_OF_ROOMS=[방의 개수] diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..15e64178 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +package.json +tsconfig.json +.prettierrc.json +.eslintrc.cjs +nodemon.json \ No newline at end of file diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..6f4e2b8c --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,80 @@ +module.exports = { + env: { + es2021: true, + node: true, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "airbnb-base", + "airbnb-typescript/base", + "plugin:mocha/recommended", + "prettier", + ], + overrides: [ + { + env: { + node: true, + mocha: true, + }, + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + project: "./tsconfig.json", + }, + plugins: ["import", "@typescript-eslint", "mocha"], + rules: { + "import/extensions": [ + "error", + "ignorePackages", + { + js: "never", // temporary fix for #159 + ts: "never", + }, + ], + "import/named": "error", + "import/no-extraneous-dependencies": [ + "error", + { + packageDir: "./", + }, + ], + "mocha/no-mocha-arrows": "off", + "no-console": "error", + "no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["../*"], + message: + "Usage of relative parent imports is not allowed. Use path alias instead.", + }, + ], + }, + ], + radix: ["error", "as-needed"], + "@typescript-eslint/consistent-type-imports": [ + "error", + { + prefer: "type-imports", + }, + ], + }, + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts"], + }, + "import/resolver": { + typescript: { + project: ["./tsconfig.json"], + }, + }, + }, +}; diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 37ae58c8..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - env: { - commonjs: true, - es2021: true, - node: true, - }, - extends: ["eslint:recommended", "prettier", "plugin:mocha/recommended"], - parserOptions: { - ecmaVersion: 13, - }, - rules: { - "no-unused-vars": 1, - "mocha/no-mocha-arrows": 0, - }, -}; diff --git a/.gitignore b/.gitignore index a7e86767..12d89e61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /node_modules +/dist +/dump +.env .env.test .env.production .env.development diff --git a/.prettierignore b/.prettierignore index dd87e2d7..e0ed084a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,5 @@ -node_modules -build +node_modules/ +dist/ +package.json +tsconfig.json +nodemon.json \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json index 873dba77..8be05812 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -16,5 +16,5 @@ "trailingComma": "es5", "useTabs": false, "vueIndentScriptAndStyle": false, - "parser": "babel" + "parser": "typescript" } diff --git a/Dockerfile b/Dockerfile index 0873829c..5ffddaa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,45 @@ -FROM node:18-alpine +# +# First stage: build the app +# +FROM node:18-alpine AS builder WORKDIR /usr/src/app -# Install curl(for taxi-watchtower) and pnpm -RUN apk update && apk add curl && npm install --global pnpm@8.8.0 +# Install pnpm +RUN npm install --global pnpm@8.8.0 # pnpm fetch does require only lockfile COPY pnpm-lock.yaml . +RUN pnpm fetch + +COPY . . +RUN pnpm install --offline +RUN pnpm build + +# +# Second stage: run the app +# +FROM node:18-alpine + +WORKDIR /usr/src/app + +# Install pnpm +RUN npm install --global pnpm@8.8.0 + +# Install curl for taxi-watchtower +RUN apk update && apk add curl -# Note: devDependencies are not fetched +# devDependencies are not fetched +COPY pnpm-lock.yaml . RUN pnpm fetch --prod -# Copy repository and install dependencies -ADD . ./ +COPY package.json . RUN pnpm install --offline --prod +# Copy the built app from the previous stage +COPY --from=builder /usr/src/app/dist ./dist + # Run container EXPOSE 80 ENV PORT 80 -CMD ["pnpm", "run", "serve"] +CMD ["pnpm", "serve"] diff --git a/app.js b/app.js deleted file mode 100644 index db9773dc..00000000 --- a/app.js +++ /dev/null @@ -1,87 +0,0 @@ -// 모듈 require -const express = require("express"); -const http = require("http"); -const { - nodeEnv, - port: httpPort, - eventConfig, - mongo: mongoUrl, -} = require("./loadenv"); -const logger = require("./src/modules/logger"); -const { connectDatabase } = require("./src/modules/stores/mongo"); -const { startSocketServer } = require("./src/modules/socket"); - -// Firebase Admin 초기설정 -require("./src/modules/fcm").initializeApp(); - -// 익스프레스 서버 생성 -const app = express(); - -// 데이터베이스 연결 -connectDatabase(mongoUrl); - -// [Middleware] request body 파싱 -app.use(express.urlencoded({ extended: false })); -app.use(express.json()); - -// reverse proxy가 설정한 헤더를 신뢰합니다. -if (nodeEnv === "production") app.set("trust proxy", 2); - -// [Middleware] CORS 설정 -app.use(require("./src/middlewares/cors")); - -// [Middleware] 세션 및 쿠키 -const session = require("./src/middlewares/session"); -app.use(session); -app.use(require("cookie-parser")()); - -// [Middleware] Timestamp 및 clientIP 확인 -app.use(require("./src/middlewares/information")); - -// [Middleware] API 접근 기록 및 응답 시간을 http response의 헤더에 기록합니다. -app.use(require("./src/middlewares/responseTime")); - -// [Router] admin 페이지는 rate limiting을 적용하지 않습니다. -app.use("/admin", require("./src/routes/admin")); - -// [Middleware] 모든 요청에 대하여 rate limiting 적용 -app.use(require("./src/middlewares/limitRate")); - -// [Router] Swagger (API 문서) -app.use("/docs", require("./src/routes/docs")); - -// [Router] 이벤트 전용 라우터입니다. -eventConfig && - app.use( - `/events/${eventConfig.mode}`, - require("./src/lottery").lotteryRouter - ); - -// [Middleware] 모든 API 요청에 대하여 origin 검증 -app.use(require("./src/middlewares/originValidator")); - -// [Router] APIs -app.use("/auth", require("./src/routes/auth")); -app.use("/logininfo", require("./src/routes/logininfo")); -app.use("/users", require("./src/routes/users")); -app.use("/rooms", require("./src/routes/rooms")); -app.use("/chats", require("./src/routes/chats")); -app.use("/locations", require("./src/routes/locations")); -app.use("/reports", require("./src/routes/reports")); -app.use("/notifications", require("./src/routes/notifications")); - -// [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. -app.use(require("./src/middlewares/errorHandler")); - -// express 서버 시작 -const serverHttp = http - .createServer(app) - .listen(httpPort, () => - logger.info(`Express server started from port ${httpPort}`) - ); - -// socket.io 서버 시작 -app.set("io", startSocketServer(serverHttp)); - -// [Schedule] 스케줄러 시작 -require("./src/schedules")(app); diff --git a/loadenv.js b/loadenv.js deleted file mode 100644 index 438b6f01..00000000 --- a/loadenv.js +++ /dev/null @@ -1,47 +0,0 @@ -// 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옴 -require("dotenv").config({ path: `./.env.${process.env.NODE_ENV}` }); - -module.exports = { - nodeEnv: process.env.NODE_ENV, // required ("production" or "development" or "test") - mongo: process.env.DB_PATH, // required - session: { - secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", // optional - expiry: 14 * 24 * 3600 * 1000, // 14일, ms 단위입니다. - }, - redis: process.env.REDIS_PATH, // optional - sparcssso: { - id: process.env.SPARCSSSO_CLIENT_ID || "", // optional - key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional - }, - port: process.env.PORT || 80, // optional (default = 80) - corsWhiteList: (process.env.CORS_WHITELIST && - JSON.parse(process.env.CORS_WHITELIST)) || [true], // optional (default = [true]) - aws: { - accessKeyId: process.env.AWS_ACCESS_KEY_ID, // required - secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, // required - s3BucketName: process.env.AWS_S3_BUCKET_NAME, // required - s3Url: - process.env.AWS_S3_URL || - `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional - }, - jwt: { - secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", - option: { - algorithm: "HS256", - // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. - // See https://github.com/sparcs-kaist/taxi-back/issues/415 - issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") - }, - TOKEN_EXPIRED: -3, - TOKEN_INVALID: -2, - }, - googleApplicationCredentials: - process.env.GOOGLE_APPLICATION_CREDENTIALS && - JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS), // optional - testAccounts: - (process.env.TEST_ACCOUNTS && JSON.parse(process.env.TEST_ACCOUNTS)) || [], // optional - slackWebhookUrl: { - report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional - }, - eventConfig: process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG), // optional -}; diff --git a/nodemon.json b/nodemon.json index 62f8de10..47114421 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,7 +1,10 @@ { - "ignore": ["node_modules/*"], - "env": { - "TZ": "Asia/Seoul", - "NODE_ENV": "development" - } -} \ No newline at end of file + "ignore": ["node_modules"], + "watch": ["src", ".env.development"], + "ext": "js,json,ts", + "exec": "ts-node --require tsconfig-paths/register src", + "env": { + "TZ": "Asia/Seoul", + "NODE_ENV": "development" + } +} diff --git a/package.json b/package.json index 4ea2a648..2882496f 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,17 @@ "main": "app.js", "scripts": { "preinstall": "npx only-allow pnpm", - "start": "cross-env TZ='Asia/Seoul' npx nodemon app.js", - "test": "npm run sample && cross-env TZ='Asia/Seoul' npm run mocha", - "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --recursive --reporter spec --exit", - "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node app.js", - "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production node", - "lint": "npx eslint --fix .", - "sample": "cd src/sampleGenerator && npm start && cd .." + "start": "nodemon", + "mocha": "cross-env TZ='Asia/Seoul' NODE_ENV=test mocha --require ts-node/register --require tsconfig-paths/register --recursive --reporter spec --exit", + "test": "pnpm run sample && cross-env TZ='Asia/Seoul' pnpm run mocha", + "build": "tsc --project tsconfig.build.json && tsc-alias", + "clean": "rimraf dist/", + "serve": "cross-env TZ='Asia/Seoul' NODE_ENV=production node dist/index.js", + "lint": "pnpm eslint .", + "runscript": "cross-env TZ='Asia/Seoul' NODE_ENV=production ts-node --require tsconfig-paths/register", + "sample": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/index.js", + "dumpDB": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/tools/dump.js", + "restoreDB": "cross-env NODE_ENV=test ts-node --require tsconfig-paths/register src/sampleGenerator/tools/restore.js" }, "engines": { "node": ">=18.0.0", @@ -27,18 +31,17 @@ "axios": "^0.27.2", "ci": "^2.2.0", "connect-mongo": "^4.6.0", - "connect-redis": "^6.1.3", + "connect-redis": "^7.1.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "cross-env": "^7.0.3", "dotenv": "^16.0.1", - "eslint-config-prettier": "^8.3.0", "express": "^4.17.1", "express-formidable": "^1.2.0", "express-rate-limit": "^7.1.0", "express-session": "^1.17.3", "express-validator": "^6.14.0", - "firebase-admin": "^11.4.1", + "firebase-admin": "^11.11.1", "jsonwebtoken": "^9.0.2", "mongoose": "^6.12.0", "node-cron": "3.0.2", @@ -56,12 +59,31 @@ "zod-to-json-schema": "^3.22.4" }, "devDependencies": { + "@types/cookie-parser": "^1.4.6", + "@types/cors": "^2.8.16", + "@types/express": "^4.17.21", + "@types/express-session": "^1.17.10", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.9.0", + "@types/node-cron": "^3.0.11", + "@types/response-time": "^2.3.8", + "@typescript-eslint/eslint-plugin": "^6.13.1", + "@typescript-eslint/parser": "^6.13.1", "chai": "^4.3.10", "eslint": "^8.22.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.0", "eslint-plugin-mocha": "^10.1.0", "mocha": "^10.2.0", "mongodb": "^4.1.0", "nodemon": "^3.0.1", - "supertest": "^6.2.4" + "rimraf": "^5.0.5", + "supertest": "^6.2.4", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", + "typescript": "^5.2.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0317beb7..90fc91f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: ^4.6.0 version: 4.6.0(express-session@1.17.3)(mongodb@4.17.1) connect-redis: - specifier: ^6.1.3 - version: 6.1.3 + specifier: ^7.1.1 + version: 7.1.1(express-session@1.17.3) cookie-parser: specifier: ^1.4.5 version: 1.4.6 @@ -41,9 +41,6 @@ dependencies: dotenv: specifier: ^16.0.1 version: 16.3.1 - eslint-config-prettier: - specifier: ^8.3.0 - version: 8.3.0(eslint@8.22.0) express: specifier: ^4.17.1 version: 4.18.2 @@ -60,8 +57,8 @@ dependencies: specifier: ^6.14.0 version: 6.15.0 firebase-admin: - specifier: ^11.4.1 - version: 11.10.1 + specifier: ^11.11.1 + version: 11.11.1 jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -109,12 +106,57 @@ dependencies: version: 3.22.4(zod@3.22.4) devDependencies: + '@types/cookie-parser': + specifier: ^1.4.6 + version: 1.4.6 + '@types/cors': + specifier: ^2.8.16 + version: 2.8.16 + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 + '@types/express-session': + specifier: ^1.17.10 + version: 1.17.10 + '@types/jsonwebtoken': + specifier: ^9.0.5 + version: 9.0.5 + '@types/node': + specifier: ^20.9.0 + version: 20.9.0 + '@types/node-cron': + specifier: ^3.0.11 + version: 3.0.11 + '@types/response-time': + specifier: ^2.3.8 + version: 2.3.8 + '@typescript-eslint/eslint-plugin': + specifier: ^6.13.1 + version: 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/parser': + specifier: ^6.13.1 + version: 6.13.1(eslint@8.22.0)(typescript@5.2.2) chai: specifier: ^4.3.10 version: 4.3.10 eslint: specifier: ^8.22.0 version: 8.22.0 + eslint-config-airbnb-base: + specifier: ^15.0.0 + version: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-config-airbnb-typescript: + specifier: ^17.1.0 + version: 17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-config-prettier: + specifier: ^8.3.0 + version: 8.3.0(eslint@8.22.0) + eslint-import-resolver-typescript: + specifier: ^3.6.1 + version: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-plugin-import: + specifier: ^2.29.0 + version: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) eslint-plugin-mocha: specifier: ^10.1.0 version: 10.1.0(eslint@8.22.0) @@ -127,15 +169,28 @@ devDependencies: nodemon: specifier: ^3.0.1 version: 3.0.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.5 supertest: specifier: ^6.2.4 version: 6.3.3 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@20.9.0)(typescript@5.2.2) + tsc-alias: + specifier: ^1.8.8 + version: 1.8.8 + typescript: + specifier: ^5.2.2 + version: 5.2.2 packages: /@aashutoshrathi/word-wrap@1.2.6: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + dev: true /@adminjs/design-system@3.1.8(@types/react@18.2.18)(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11): resolution: {integrity: sha512-M0l8NXoHKFoJ9XLv6BkrgRPnE0hCYNYWVNiQKA4qOpzifB2LAPAViqQ36Qyxgz1mL9nnzl7OJpGlb8cHSrIajg==} @@ -763,10 +818,10 @@ packages: '@babel/helpers': 7.22.6 '@babel/parser': 7.22.7 '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 + '@babel/traverse': 7.22.8(supports-color@5.5.0) '@babel/types': 7.22.5 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -850,7 +905,7 @@ packages: '@babel/core': 7.22.9 '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.22.9) '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) lodash.debounce: 4.0.8 resolve: 1.22.4 transitivePeerDependencies: @@ -991,7 +1046,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.22.5 - '@babel/traverse': 7.22.8 + '@babel/traverse': 7.22.8(supports-color@5.5.0) '@babel/types': 7.22.5 transitivePeerDependencies: - supports-color @@ -2027,24 +2082,6 @@ packages: '@babel/types': 7.22.5 dev: false - /@babel/traverse@7.22.8: - resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.22.5 - '@babel/generator': 7.22.9 - '@babel/helper-environment-visitor': 7.22.5 - '@babel/helper-function-name': 7.22.5 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.22.7 - '@babel/types': 7.22.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - dev: false - /@babel/traverse@7.22.8(supports-color@5.5.0): resolution: {integrity: sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw==} engines: {node: '>=6.9.0'} @@ -2098,6 +2135,13 @@ packages: engines: {node: '>=0.1.90'} dev: false + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + /@dabh/diagnostics@2.0.3: resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} dependencies: @@ -2209,12 +2253,27 @@ packages: resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} dev: false + /@eslint-community/eslint-utils@4.4.0(eslint@8.22.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.22.0 + eslint-visitor-keys: 3.4.2 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint/eslintrc@1.4.1: resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -2224,6 +2283,7 @@ packages: strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color + dev: true /@fastify/busboy@1.2.1: resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} @@ -2379,7 +2439,7 @@ packages: requiresBuild: true dependencies: '@grpc/proto-loader': 0.7.8 - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false optional: true @@ -2423,16 +2483,19 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color + dev: true /@humanwhocodes/gitignore-to-minimatch@1.0.2: resolution: {integrity: sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==} + dev: true /@humanwhocodes/object-schema@1.2.1: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true /@hypnosphi/create-react-context@0.3.1(prop-types@15.8.1)(react@18.2.0): resolution: {integrity: sha512-V1klUed202XahrWJLLOT3EXNeCpFHCcJntdFGI15ntCwau+jfT386w7OFTMaCqOgXUH1fa0w/I1oZs+i/Rfr0A==} @@ -2446,6 +2509,18 @@ packages: warning: 4.0.3 dev: false + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -2458,7 +2533,6 @@ packages: /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} - dev: false /@jridgewell/set-array@1.1.2: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} @@ -2478,7 +2552,6 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: false /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} @@ -2487,6 +2560,13 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: false + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + /@jsdoc/salty@0.2.5: resolution: {integrity: sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==} engines: {node: '>=v12.0.0'} @@ -2509,10 +2589,12 @@ packages: dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 + dev: true /@nodelib/fs.stat@2.0.5: resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} + dev: true /@nodelib/fs.walk@1.2.8: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} @@ -2520,6 +2602,14 @@ packages: dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true /@popperjs/core@2.11.8: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -3591,6 +3681,22 @@ packages: dev: false optional: true + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + /@types/babel-core@6.25.7: resolution: {integrity: sha512-WPnyzNFVRo6bxpr7bcL27qXtNKNQ3iToziNBpibaXHyKGWQA0+tTLt73QQxC/5zzbM544ih6Ni5L5xrck6rGwg==} dependencies: @@ -3634,24 +3740,27 @@ packages: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 + + /@types/cookie-parser@1.4.6: + resolution: {integrity: sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==} + dependencies: + '@types/express': 4.17.21 + dev: true /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: false - /@types/cors@2.8.13: - resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + /@types/cors@2.8.16: + resolution: {integrity: sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==} dependencies: - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/estree@0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} @@ -3660,27 +3769,31 @@ packages: /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 '@types/send': 0.17.1 - dev: false - /@types/express@4.17.17: - resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + /@types/express-session@1.17.10: + resolution: {integrity: sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw==} + dependencies: + '@types/express': 4.17.21 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} dependencies: '@types/body-parser': 1.19.2 '@types/express-serve-static-core': 4.17.35 '@types/qs': 6.9.7 '@types/serve-static': 1.15.2 - dev: false /@types/glob@8.1.0: resolution: {integrity: sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==} requiresBuild: true dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false optional: true @@ -3693,13 +3806,19 @@ packages: /@types/http-errors@2.0.1: resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==} - dev: false - /@types/jsonwebtoken@9.0.2: - resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + dev: true + + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} dependencies: - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/linkify-it@3.0.2: resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} @@ -3730,7 +3849,6 @@ packages: /@types/mime@1.3.2: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} - dev: false /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -3738,8 +3856,14 @@ packages: dev: false optional: true - /@types/node@20.4.7: - resolution: {integrity: sha512-bUBrPjEry2QUTsnuEjzjbS7voGWCc30W0qzgMf90GPeDGFRakvrz47ju+oqDAKCXLUCe39u57/ORMl/O/04/9g==} + /@types/node-cron@3.0.11: + resolution: {integrity: sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==} + dev: true + + /@types/node@20.9.0: + resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} + dependencies: + undici-types: 5.26.5 /@types/object.omit@3.0.0: resolution: {integrity: sha512-I27IoPpH250TUzc9FzXd0P1BV/BMJuzqD3jOz98ehf9dQqGkxlq+hO1bIqZGWqCg5bVOy0g4AUVJtnxe0klDmw==} @@ -3759,11 +3883,9 @@ packages: /@types/qs@6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} - dev: false /@types/range-parser@1.2.4: resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} - dev: false /@types/react-transition-group@4.4.6: resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} @@ -3782,15 +3904,22 @@ packages: /@types/resolve@1.17.1: resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false + /@types/response-time@2.3.8: + resolution: {integrity: sha512-7qGaNYvdxc0zRab8oHpYx7AW17qj+G0xuag1eCrw3M2VWPJQ/HyKaaghWygiaOUl0y9x7QGQwppDpqLJ5V9pzw==} + dependencies: + '@types/express': 4.17.21 + '@types/node': 20.9.0 + dev: true + /@types/rimraf@3.0.2: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} requiresBuild: true dependencies: '@types/glob': 8.1.0 - '@types/node': 20.4.7 + '@types/node': 20.9.0 dev: false optional: true @@ -3798,20 +3927,22 @@ packages: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: false + /@types/semver@7.5.6: + resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==} + dev: true + /@types/send@0.17.1: resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} dependencies: '@types/mime': 1.3.2 - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/serve-static@1.15.2: resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==} dependencies: '@types/http-errors': 2.0.1 '@types/mime': 1.3.2 - '@types/node': 20.4.7 - dev: false + '@types/node': 20.9.0 /@types/throttle-debounce@2.1.0: resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} @@ -3831,9 +3962,140 @@ packages: /@types/whatwg-url@8.2.2: resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 '@types/webidl-conversions': 7.0.0 + /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.13.1 + '@typescript-eslint/type-utils': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.13.1 + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.22.0 + graphemer: 1.4.0 + ignore: 5.2.4 + natural-compare: 1.4.0 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@6.13.1(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 6.13.1 + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.13.1 + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.22.0 + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@6.13.1: + resolution: {integrity: sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/visitor-keys': 6.13.1 + dev: true + + /@typescript-eslint/type-utils@6.13.1(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) + '@typescript-eslint/utils': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + debug: 4.3.4(supports-color@5.5.0) + eslint: 8.22.0 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@6.13.1: + resolution: {integrity: sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@6.13.1(typescript@5.2.2): + resolution: {integrity: sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/visitor-keys': 6.13.1 + debug: 4.3.4(supports-color@5.5.0) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + ts-api-utils: 1.0.3(typescript@5.2.2) + typescript: 5.2.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@6.13.1(eslint@8.22.0)(typescript@5.2.2): + resolution: {integrity: sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.22.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.6 + '@typescript-eslint/scope-manager': 6.13.1 + '@typescript-eslint/types': 6.13.1 + '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) + eslint: 8.22.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@6.13.1: + resolution: {integrity: sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 6.13.1 + eslint-visitor-keys: 3.4.2 + dev: true + /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} dev: true @@ -3862,6 +4124,11 @@ packages: dependencies: acorn: 8.10.0 + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + /acorn@8.10.0: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} @@ -3924,7 +4191,7 @@ packages: engines: {node: '>= 6.0.0'} requiresBuild: true dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -3937,6 +4204,7 @@ packages: fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 + dev: true /ansi-colors@4.1.1: resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} @@ -3947,6 +4215,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -3960,6 +4233,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -3968,16 +4246,83 @@ packages: picomatch: 2.3.1 dev: true + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + /array-buffer-byte-length@1.0.0: + resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} + dependencies: + call-bind: 1.0.5 + is-array-buffer: 3.0.2 + dev: true + /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} dev: false + /array-includes@3.1.7: + resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-string: 1.0.7 + dev: true + /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + dev: true + + /array.prototype.findlastindex@1.2.3: + resolution: {integrity: sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + get-intrinsic: 1.2.2 + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + es-shim-unscopables: 1.0.2 + dev: true + + /arraybuffer.prototype.slice@1.0.2: + resolution: {integrity: sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + is-array-buffer: 3.0.2 + is-shared-array-buffer: 1.0.2 + dev: true /arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} @@ -4021,7 +4366,6 @@ packages: /available-typed-arrays@1.0.5: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - dev: false /aws-sdk@2.1430.0: resolution: {integrity: sha512-827BjW9Q9NwUucZBHHU64dh96ihE857LC0ZOEub0C5wjxujoEqET0i4qJR7k+/wn8tFMNtWO0rIGCSGSmgrm5A==} @@ -4190,6 +4534,7 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 + dev: true /browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} @@ -4250,6 +4595,13 @@ packages: function-bind: 1.1.1 get-intrinsic: 1.2.1 + /call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + dependencies: + function-bind: 1.1.2 + get-intrinsic: 1.2.2 + set-function-length: 1.1.1 + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -4445,6 +4797,11 @@ packages: engines: {node: '>= 6'} dev: false + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + dev: true + /commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} dev: false @@ -4465,6 +4822,10 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /confusing-browser-globals@1.0.11: + resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} + dev: true + /connect-mongo@4.6.0(express-session@1.17.3)(mongodb@4.17.1): resolution: {integrity: sha512-8new4Z7NLP3CGP65Aw6ls3xDBeKVvHRSh39CXuDZTQsvpeeU9oNMzfFgvqmHqZ6gWpxIl663RyoVEmCAGf1yOg==} engines: {node: '>=10'} @@ -4472,7 +4833,7 @@ packages: express-session: ^1.17.1 mongodb: ^4.1.0 dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) express-session: 1.17.3 kruptein: 3.0.6 mongodb: 4.17.1 @@ -4480,9 +4841,13 @@ packages: - supports-color dev: false - /connect-redis@6.1.3: - resolution: {integrity: sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw==} - engines: {node: '>=12'} + /connect-redis@7.1.1(express-session@1.17.3): + resolution: {integrity: sha512-M+z7alnCJiuzKa8/1qAYdGUXHYfDnLolOGAUjOioB07pP39qxjG+X9ibsud7qUBc4jMV5Mcy3ugGv8eFcgamJQ==} + engines: {node: '>=16'} + peerDependencies: + express-session: '>=1' + dependencies: + express-session: 1.17.3 dev: false /content-disposition@0.5.4: @@ -4563,6 +4928,10 @@ packages: yaml: 1.10.2 dev: false + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} dev: false @@ -4649,17 +5018,6 @@ packages: supports-color: 5.5.0 dev: true - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -4671,7 +5029,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 5.5.0 - dev: false /debug@4.3.4(supports-color@8.1.1): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} @@ -4712,6 +5069,23 @@ packages: clone: 1.0.4 dev: false + /define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + has-property-descriptors: 1.0.1 + object-keys: 1.1.1 + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -4738,6 +5112,11 @@ packages: wrappy: 1.0.2 dev: true + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + /diff@5.0.0: resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} engines: {node: '>=0.3.1'} @@ -4748,12 +5127,21 @@ packages: engines: {node: '>=8'} dependencies: path-type: 4.0.0 + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true /doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} dependencies: esutils: 2.0.3 + dev: true /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} @@ -4778,6 +5166,10 @@ packages: dev: false optional: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -4795,6 +5187,10 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} dev: false @@ -4822,13 +5218,13 @@ packages: engines: {node: '>=10.2.0'} dependencies: '@types/cookie': 0.4.1 - '@types/cors': 2.8.13 - '@types/node': 20.4.7 + '@types/cors': 2.8.16 + '@types/node': 20.9.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.4.2 cors: 2.8.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) engine.io-parser: 5.2.1 ws: 8.11.0 transitivePeerDependencies: @@ -4837,9 +5233,17 @@ packages: - utf-8-validate dev: false - /ent@2.2.0: - resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} - requiresBuild: true + /enhanced-resolve@5.15.0: + resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} + engines: {node: '>=10.13.0'} + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + dev: true + + /ent@2.2.0: + resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} + requiresBuild: true dev: false optional: true @@ -4860,6 +5264,75 @@ packages: is-arrayish: 0.2.1 dev: false + /es-abstract@1.22.3: + resolution: {integrity: sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.0 + arraybuffer.prototype.slice: 1.0.2 + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + es-set-tostringtag: 2.0.2 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.2 + get-symbol-description: 1.0.0 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + internal-slot: 1.0.6 + is-array-buffer: 3.0.2 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-typed-array: 1.1.12 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.5.1 + safe-array-concat: 1.0.1 + safe-regex-test: 1.0.0 + string.prototype.trim: 1.2.8 + string.prototype.trimend: 1.0.7 + string.prototype.trimstart: 1.0.7 + typed-array-buffer: 1.0.0 + typed-array-byte-length: 1.0.0 + typed-array-byte-offset: 1.0.0 + typed-array-length: 1.0.4 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.13 + dev: true + + /es-set-tostringtag@2.0.2: + resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + has-tostringtag: 1.0.0 + hasown: 2.0.0 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.0 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -4903,6 +5376,36 @@ packages: dev: false optional: true + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.0)(eslint@8.22.0): + resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} + engines: {node: ^10.12.0 || >=12.0.0} + peerDependencies: + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.2 + dependencies: + confusing-browser-globals: 1.0.11 + eslint: 8.22.0 + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + object.assign: 4.1.4 + object.entries: 1.1.7 + semver: 6.3.1 + dev: true + + /eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0): + resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0 + '@typescript-eslint/parser': ^5.0.0 || ^6.0.0 + eslint: ^7.32.0 || ^8.2.0 + eslint-plugin-import: ^2.25.3 + dependencies: + '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.13.1)(eslint@8.22.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + eslint: 8.22.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.22.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + dev: true + /eslint-config-prettier@8.3.0(eslint@8.22.0): resolution: {integrity: sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==} hasBin: true @@ -4910,7 +5413,105 @@ packages: eslint: '>=7.0.0' dependencies: eslint: 8.22.0 - dev: false + dev: true + + /eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + dependencies: + debug: 3.2.7(supports-color@5.5.0) + is-core-module: 2.13.1 + resolve: 1.22.4 + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0): + resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.4(supports-color@5.5.0) + enhanced-resolve: 5.15.0 + eslint: 8.22.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + fast-glob: 3.3.1 + get-tsconfig: 4.7.2 + is-core-module: 2.13.1 + is-glob: 4.0.3 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0): + resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + debug: 3.2.7(supports-color@5.5.0) + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.1)(eslint-plugin-import@2.29.0)(eslint@8.22.0) + transitivePeerDependencies: + - supports-color + dev: true + + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0): + resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + '@typescript-eslint/parser': 6.13.1(eslint@8.22.0)(typescript@5.2.2) + array-includes: 3.1.7 + array.prototype.findlastindex: 1.2.3 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7(supports-color@5.5.0) + doctrine: 2.1.0 + eslint: 8.22.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.22.0) + hasown: 2.0.0 + is-core-module: 2.13.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.7 + object.groupby: 1.0.1 + object.values: 1.1.7 + semver: 6.3.1 + tsconfig-paths: 3.14.2 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true /eslint-plugin-mocha@10.1.0(eslint@8.22.0): resolution: {integrity: sha512-xLqqWUF17llsogVOC+8C6/jvQ+4IoOREbN7ZCHuOHuD6cT5cDD4h7f2LgsZuzMAiwswWE21tO7ExaknHVDrSkw==} @@ -4929,6 +5530,7 @@ packages: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 + dev: true /eslint-utils@3.0.0(eslint@8.22.0): resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} @@ -4938,10 +5540,12 @@ packages: dependencies: eslint: 8.22.0 eslint-visitor-keys: 2.1.0 + dev: true /eslint-visitor-keys@2.1.0: resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} engines: {node: '>=10'} + dev: true /eslint-visitor-keys@3.4.2: resolution: {integrity: sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==} @@ -4958,7 +5562,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -4993,6 +5597,7 @@ packages: v8-compile-cache: 2.3.0 transitivePeerDependencies: - supports-color + dev: true /espree@9.6.1: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} @@ -5015,12 +5620,14 @@ packages: engines: {node: '>=0.10'} dependencies: estraverse: 5.3.0 + dev: true /esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} dependencies: estraverse: 5.3.0 + dev: true /estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} @@ -5159,9 +5766,11 @@ packages: glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 + dev: true /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true /fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} @@ -5197,6 +5806,7 @@ packages: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 + dev: true /faye-websocket@0.11.4: resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} @@ -5214,6 +5824,7 @@ packages: engines: {node: ^10.12.0 || >=12.0.0} dependencies: flat-cache: 3.0.4 + dev: true /file-stream-rotator@0.6.1: resolution: {integrity: sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==} @@ -5226,6 +5837,7 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + dev: true /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} @@ -5268,15 +5880,16 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 + dev: true - /firebase-admin@11.10.1: - resolution: {integrity: sha512-atv1E6GbuvcvWaD3eHwrjeP5dAVs+EaHEJhu9CThMzPY6In8QYDiUR6tq5SwGl4SdA/GcAU0nhwWc/FSJsAzfQ==} + /firebase-admin@11.11.1: + resolution: {integrity: sha512-UyEbq+3u6jWzCYbUntv/HuJiTixwh36G1R9j0v71mSvGAx/YZEWEW7uSGLYxBYE6ckVRQoKMr40PYUEzrm/4dg==} engines: {node: '>=14'} dependencies: '@fastify/busboy': 1.2.1 '@firebase/database-compat': 0.3.4 '@firebase/database-types': 0.10.4 - '@types/node': 20.4.7 + '@types/node': 20.9.0 jsonwebtoken: 9.0.2 jwks-rsa: 3.0.1 node-forge: 1.3.1 @@ -5295,6 +5908,7 @@ packages: dependencies: flatted: 3.2.7 rimraf: 3.0.2 + dev: true /flat@5.0.2: resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} @@ -5302,6 +5916,7 @@ packages: /flatted@3.2.7: resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true /fn.name@1.1.0: resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} @@ -5321,7 +5936,14 @@ packages: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: is-callable: 1.2.7 - dev: false + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true /form-data@4.0.0: resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} @@ -5368,9 +5990,26 @@ packages: /function-bind@1.1.1: resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + functions-have-names: 1.2.3 + dev: true + /functional-red-black-tree@1.0.1: resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + /gaxios@5.1.3: resolution: {integrity: sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==} engines: {node: '>=12'} @@ -5425,17 +6064,53 @@ packages: has-proto: 1.0.1 has-symbols: 1.0.3 + /get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + dependencies: + function-bind: 1.1.2 + has-proto: 1.0.1 + has-symbols: 1.0.3 + hasown: 2.0.0 + + /get-symbol-description@1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + dev: true + + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 + dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 + dev: true + + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true /glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} @@ -5461,6 +6136,7 @@ packages: /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported requiresBuild: true dependencies: fs.realpath: 1.0.0 @@ -5481,6 +6157,14 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -5492,6 +6176,7 @@ packages: ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 + dev: true /google-auth-library@8.9.0: resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==} @@ -5543,6 +6228,7 @@ packages: /google-p12-pem@4.0.1: resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==} engines: {node: '>=12.0.0'} + deprecated: Package is no longer maintained hasBin: true requiresBuild: true dependencies: @@ -5554,16 +6240,18 @@ packages: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: get-intrinsic: 1.2.1 - dev: false /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} requiresBuild: true - dev: false - optional: true /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true /gtoken@6.1.2: resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} @@ -5583,6 +6271,10 @@ packages: resolution: {integrity: sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==} dev: false + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -5591,6 +6283,11 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + /has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + dependencies: + get-intrinsic: 1.2.2 + /has-proto@1.0.1: resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} engines: {node: '>= 0.4'} @@ -5604,7 +6301,6 @@ packages: engines: {node: '>= 0.4'} dependencies: has-symbols: 1.0.3 - dev: false /has@1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} @@ -5612,6 +6308,12 @@ packages: dependencies: function-bind: 1.1.1 + /hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -5656,7 +6358,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -5668,7 +6370,7 @@ packages: requiresBuild: true dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -5697,6 +6399,7 @@ packages: /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -5708,6 +6411,7 @@ packages: /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + dev: true /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} @@ -5718,6 +6422,15 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + /internal-slot@1.0.6: + resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.2 + hasown: 2.0.0 + side-channel: 1.0.4 + dev: true + /ip@2.0.0: resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} @@ -5734,6 +6447,14 @@ packages: has-tostringtag: 1.0.0 dev: false + /is-array-buffer@3.0.2: + resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + /is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} dev: false @@ -5742,6 +6463,12 @@ packages: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} dev: false + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + /is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -5749,6 +6476,14 @@ packages: binary-extensions: 2.2.0 dev: true + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + /is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -5759,13 +6494,24 @@ packages: /is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - dev: false /is-core-module@2.13.0: resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} dependencies: has: 1.0.3 - dev: false + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.0 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true /is-extendable@1.0.1: resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} @@ -5777,6 +6523,7 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + dev: true /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -5794,6 +6541,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + dev: true /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} @@ -5804,9 +6552,22 @@ packages: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} dev: false + /is-negative-zero@2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + dev: true /is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} @@ -5826,6 +6587,20 @@ packages: '@types/estree': 0.0.39 dev: false + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer@1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.5 + dev: true + /is-stream-ended@0.1.4: resolution: {integrity: sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==} requiresBuild: true @@ -5837,21 +6612,44 @@ packages: engines: {node: '>=8'} dev: false + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + /is-typed-array@1.1.12: resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} engines: {node: '>= 0.4'} dependencies: - which-typed-array: 1.1.11 - dev: false + which-typed-array: 1.1.13 /is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.5 + dev: true + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} dev: false + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -5860,11 +6658,20 @@ packages: engines: {node: '>=0.10.0'} dev: false + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + /jest-worker@26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.4.7 + '@types/node': 20.9.0 merge-stream: 2.0.0 supports-color: 7.2.0 dev: false @@ -5887,6 +6694,7 @@ packages: hasBin: true dependencies: argparse: 2.0.1 + dev: true /js2xmlparser@4.0.2: resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} @@ -5945,9 +6753,18 @@ packages: /json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: true /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} @@ -5997,9 +6814,9 @@ packages: resolution: {integrity: sha512-UUOZ0CVReK1QVU3rbi9bC7N5/le8ziUj0A2ef1Q0M7OPD2KvjEYizptqIxGIo6fSLYDkqBrazILS18tYuRc8gw==} engines: {node: '>=14'} dependencies: - '@types/express': 4.17.17 - '@types/jsonwebtoken': 9.0.2 - debug: 4.3.4 + '@types/express': 4.17.21 + '@types/jsonwebtoken': 9.0.5 + debug: 4.3.4(supports-color@5.5.0) jose: 4.14.4 limiter: 1.1.5 lru-memoizer: 2.2.0 @@ -6068,6 +6885,7 @@ packages: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + dev: true /limiter@1.1.5: resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} @@ -6108,6 +6926,7 @@ packages: engines: {node: '>=10'} dependencies: p-locate: 5.0.0 + dev: true /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} @@ -6153,6 +6972,7 @@ packages: /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true /lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -6213,6 +7033,13 @@ packages: get-func-name: 2.0.2 dev: true + /lru-cache@10.0.2: + resolution: {integrity: sha512-Yj9mA8fPiVgOUpByoTZO5pNrcl5Yk37FcSHsUINpAsaBIEZIuqcCclDZJCVxqQShDsmYX8QG63svJiTbOATZwg==} + engines: {node: 14 || >=16.14} + dependencies: + semver: 7.5.4 + dev: true + /lru-cache@4.0.2: resolution: {integrity: sha512-uQw9OqphAGiZhkuPlpFGmdTU2tEuhxTourM/19qGJrxBPHAr/f8BT1a0i/lOclESnGatdJG/UCkP9kZB/Lh1iw==} dependencies: @@ -6255,7 +7082,6 @@ packages: /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - dev: false /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2): resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} @@ -6330,6 +7156,7 @@ packages: /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + dev: true /methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} @@ -6341,6 +7168,7 @@ packages: dependencies: braces: 3.0.2 picomatch: 2.3.1 + dev: true /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} @@ -6402,11 +7230,21 @@ packages: dev: false optional: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} requiresBuild: true - dev: false - optional: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} @@ -6492,7 +7330,7 @@ packages: resolution: {integrity: sha512-J5heI+P08I6VJ2Ky3+33IpCdAvlYGTSUjwTPxkAr8i8EoduPMBX2OY/wa3IKZIQl7MU4SbFk8ndgSKyB/cl1zA==} engines: {node: '>=12.0.0'} dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -6507,6 +7345,11 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + /mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + dev: true + /nanoid@3.3.3: resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6515,6 +7358,7 @@ packages: /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} @@ -6621,6 +7465,52 @@ packages: /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.7: + resolution: {integrity: sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.fromentries@2.0.7: + resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /object.groupby@1.0.1: + resolution: {integrity: sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + get-intrinsic: 1.2.2 + dev: true + /object.omit@3.0.0: resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==} engines: {node: '>=0.10.0'} @@ -6635,6 +7525,15 @@ packages: isobject: 3.0.1 dev: false + /object.values@1.1.7: + resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -6689,6 +7588,7 @@ packages: levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 + dev: true /ora@5.4.1: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} @@ -6734,6 +7634,7 @@ packages: engines: {node: '>=10'} dependencies: p-limit: 3.1.0 + dev: true /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} @@ -6769,6 +7670,7 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -6780,7 +7682,14 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: false + + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.0.2 + minipass: 7.0.4 + dev: true /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -6823,6 +7732,13 @@ packages: find-up: 3.0.0 dev: false + /plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + dependencies: + queue-lit: 1.5.2 + dev: true + /polished@3.7.2: resolution: {integrity: sha512-pQKtpZGmsZrW8UUpQMAnR7s3ppHeMQVNyMDKtUyKwuvDmklzcEyM5Kllb3JyE/sE/x7arDmyd35i+4vp99H6sQ==} engines: {node: '>=10'} @@ -6844,6 +7760,7 @@ packages: /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + dev: true /prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -7042,7 +7959,7 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.4.7 + '@types/node': 20.9.0 long: 5.2.3 dev: false optional: true @@ -7062,7 +7979,7 @@ packages: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 20.4.7 + '@types/node': 20.9.0 long: 5.2.3 dev: false optional: true @@ -7109,8 +8026,14 @@ packages: deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. dev: false + /queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true /raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} @@ -7412,9 +8335,19 @@ packages: '@babel/runtime': 7.22.6 dev: false + /regexp.prototype.flags@1.5.1: + resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + set-function-name: 2.0.1 + dev: true + /regexpp@3.2.0: resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} engines: {node: '>=8'} + dev: true /regexpu-core@5.3.2: resolution: {integrity: sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==} @@ -7451,6 +8384,10 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /resolve@1.22.4: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} hasBin: true @@ -7458,7 +8395,6 @@ packages: is-core-module: 2.13.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - dev: false /response-time@2.3.2: resolution: {integrity: sha512-MUIDaDQf+CVqflfTdQ5yam+aYCkXj1PY8fjlPDQ6ppxJlmgZb864pHtA750mayywNg8tx4rS7qH9JXd/OF+3gw==} @@ -7481,7 +8417,7 @@ packages: engines: {node: '>=12'} requiresBuild: true dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) extend: 3.0.2 transitivePeerDependencies: - supports-color @@ -7498,6 +8434,7 @@ packages: /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true /rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} @@ -7505,6 +8442,14 @@ packages: dependencies: glob: 7.2.3 + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: true + /rollup-plugin-terser@7.0.2(rollup@2.79.1): resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser @@ -7534,10 +8479,29 @@ packages: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 + dev: true + + /safe-array-concat@1.0.1: + resolution: {integrity: sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + /safe-regex-test@1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-regex: 1.1.4 + dev: true + /safe-stable-stringify@2.4.3: resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} engines: {node: '>=10'} @@ -7565,7 +8529,6 @@ packages: /semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - dev: false /semver@7.5.4: resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} @@ -7619,6 +8582,24 @@ packages: - supports-color dev: false + /set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + get-intrinsic: 1.2.2 + gopd: 1.0.1 + has-property-descriptors: 1.0.1 + + /set-function-name@2.0.1: + resolution: {integrity: sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.1 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.1 + dev: true + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} dev: false @@ -7659,6 +8640,11 @@ packages: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} dev: false + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} dependencies: @@ -7694,7 +8680,7 @@ packages: engines: {node: '>=10.0.0'} dependencies: '@socket.io/component-emitter': 3.1.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) transitivePeerDependencies: - supports-color dev: false @@ -7706,7 +8692,7 @@ packages: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) engine.io: 6.5.2 socket.io-adapter: 2.5.2 socket.io-parser: 4.2.4 @@ -7783,6 +8769,40 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string.prototype.trim@1.2.8: + resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimend@1.0.7: + resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + + /string.prototype.trimstart@1.0.7: + resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} + dependencies: + call-bind: 1.0.5 + define-properties: 1.2.1 + es-abstract: 1.22.3 + dev: true + /string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: @@ -7795,6 +8815,18 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -7861,7 +8893,7 @@ packages: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 - debug: 4.3.4 + debug: 4.3.4(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.0 formidable: 2.1.2 @@ -7905,7 +8937,6 @@ packages: /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - dev: false /swagger-ui-dist@5.3.1: resolution: {integrity: sha512-El78OvXp9zMasfPrshtkW1CRx8AugAKoZuGGOTW+8llJzOV1RtDJYqQRz/6+2OakjeWWnZuRlN2Qj1Y0ilux3w==} @@ -7921,6 +8952,11 @@ packages: swagger-ui-dist: 5.3.1 dev: false + /tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + dev: true + /teeny-request@8.0.3: resolution: {integrity: sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==} engines: {node: '>=12'} @@ -7962,6 +8998,7 @@ packages: /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true /throttle-debounce@3.0.1: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} @@ -7997,6 +9034,7 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + dev: true /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -8027,6 +9065,67 @@ packages: engines: {node: '>= 14.0.0'} dev: false + /ts-api-utils@1.0.3(typescript@5.2.2): + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 5.2.2 + dev: true + + /ts-node@10.9.2(@types/node@20.9.0)(typescript@5.2.2): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.9.0 + acorn: 8.10.0 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.2.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tsc-alias@1.8.8: + resolution: {integrity: sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q==} + hasBin: true + dependencies: + chokidar: 3.5.3 + commander: 9.5.0 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + dev: true + + /tsconfig-paths@3.14.2: + resolution: {integrity: sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==} + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} requiresBuild: true @@ -8049,6 +9148,7 @@ packages: engines: {node: '>= 0.8.0'} dependencies: prelude-ls: 1.2.1 + dev: true /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} @@ -8058,6 +9158,7 @@ packages: /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} + dev: true /type-fest@2.19.0: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} @@ -8072,6 +9173,50 @@ packages: mime-types: 2.1.35 dev: false + /typed-array-buffer@1.0.0: + resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + get-intrinsic: 1.2.2 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-length@1.0.0: + resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-byte-offset@1.0.0: + resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + has-proto: 1.0.1 + is-typed-array: 1.1.12 + dev: true + + /typed-array-length@1.0.4: + resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} + dependencies: + call-bind: 1.0.5 + for-each: 0.3.3 + is-typed-array: 1.1.12 + dev: true + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true + /uc.micro@1.0.6: resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} dev: false @@ -8091,6 +9236,15 @@ packages: random-bytes: 1.0.0 dev: false + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.5 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + /undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} dev: true @@ -8101,6 +9255,9 @@ packages: dev: false optional: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /unicode-canonical-property-names-ecmascript@2.0.0: resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==} engines: {node: '>=4'} @@ -8144,6 +9301,7 @@ packages: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: punycode: 2.3.0 + dev: true /url@0.10.3: resolution: {integrity: sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==} @@ -8214,8 +9372,13 @@ packages: hasBin: true dev: false + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + /v8-compile-cache@2.3.0: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} + dev: true /validator@13.11.0: resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} @@ -8288,6 +9451,16 @@ packages: dev: false optional: true + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + /which-typed-array@1.1.11: resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} engines: {node: '>= 0.4'} @@ -8299,6 +9472,16 @@ packages: has-tostringtag: 1.0.0 dev: false + /which-typed-array@1.1.13: + resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.5 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 + /which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -8364,6 +9547,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -8478,6 +9670,11 @@ packages: dev: false optional: true + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/scripts/chatPaymentSettlementUpdater.js b/scripts/chatPaymentSettlementUpdater.js index 76e43682..1b1d11dd 100644 --- a/scripts/chatPaymentSettlementUpdater.js +++ b/scripts/chatPaymentSettlementUpdater.js @@ -3,7 +3,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/449 const { MongoClient } = require("mongodb"); -const { mongo: mongoUrl } = require("../loadenv"); +const { mongo: mongoUrl } = require("@/loadenv"); const client = new MongoClient(mongoUrl); const db = client.db("taxi"); diff --git a/scripts/profileImageUrlUpdater.js b/scripts/profileImageUrlUpdater.js index 78ebe778..5814f609 100644 --- a/scripts/profileImageUrlUpdater.js +++ b/scripts/profileImageUrlUpdater.js @@ -2,7 +2,7 @@ // https://github.com/sparcs-kaist/taxi-back/issues/173 const { MongoClient } = require("mongodb"); -const { mongo: mongoUrl, aws: awsEnv } = require("../loadenv"); +const { mongo: mongoUrl, aws: awsEnv } = require("@/loadenv"); const time = Date.now(); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..ced953e8 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,110 @@ +// 모듈 import +import express from "express"; +import cookieParser from "cookie-parser"; +import http from "http"; + +import { nodeEnv, mongo as mongoUrl, port as httpPort } from "@/loadenv"; +import { + corsMiddleware, + errorHandler, + informationMiddleware, + limitRateMiddleware, + originValidatorMiddleware, + responseTimeMiddleware, + sessionMiddleware, +} from "@/middlewares"; +import { + adminRouter, + authRouter, + chatRouter, + docsRouter, + fareRouter, + locationRouter, + logininfoRouter, + notificationRouter, + reportRouter, + roomRouter, + userRouter, +} from "@/routes"; + +import { initializeApp as initializeFirebase } from "@/modules/fcm"; +import { initializeDatabase as initializeFareDatabase } from "@/modules/fare"; +import logger from "@/modules/logger"; +import { startSocketServer } from "@/modules/socket"; +import { connectDatabase } from "@/modules/stores/mongo"; +import registerSchedules from "@/schedules"; + +// Firebase Admin 초기설정 +initializeFirebase(); + +// 데이터베이스 연결 +connectDatabase(mongoUrl); + +// 익스프레스 서버 생성 +const app = express(); + +// [Middleware] request body 파싱 +app.use(express.urlencoded({ extended: false })); +app.use(express.json()); + +// reverse proxy가 설정한 헤더를 신뢰합니다. +if (nodeEnv === "production") app.set("trust proxy", 2); + +// [Middleware] CORS 설정 +app.use(corsMiddleware); + +// [Middleware] 세션 및 쿠키 +app.use(sessionMiddleware); +app.use(cookieParser()); + +// [Middleware] Timestamp 및 clientIP 확인 +app.use(informationMiddleware); + +// [Middleware] API 접근 기록 및 응답 시간을 http response의 헤더에 기록합니다. +app.use(responseTimeMiddleware); + +// [Router] admin 페이지는 rate limiting을 적용하지 않습니다. +app.use("/admin", adminRouter); + +// [Middleware] 모든 요청에 대하여 rate limiting 적용 +app.use(limitRateMiddleware); + +// [Router] Swagger (API 문서) +app.use("/docs", docsRouter); + +// [Router] 이벤트 전용 라우터입니다. +// eventConfig && +// app.use(`/events/${eventConfig.mode}`, require("@/lottery").lotteryRouter); + +// [Middleware] 모든 API 요청에 대하여 origin 검증 +app.use(originValidatorMiddleware); + +// [Router] APIs +app.use("/auth", authRouter); +app.use("/chats", chatRouter); +app.use("/fare", fareRouter); +app.use("/locations", locationRouter); +app.use("/logininfo", logininfoRouter); +app.use("/notifications", notificationRouter); +app.use("/reports", reportRouter); +app.use("/rooms", roomRouter); +app.use("/users", userRouter); + +// [Middleware] 전역 에러 핸들러. 에러 핸들러는 router들보다 아래에 등록되어야 합니다. +app.use(errorHandler); + +// express 서버 시작 +const serverHttp = http + .createServer(app) + .listen(httpPort, () => + logger.info(`Express server started from port ${httpPort}`) + ); + +// socket.io 서버 시작 +app.set("io", startSocketServer(serverHttp)); + +// [Schedule] 스케줄러 시작 +registerSchedules(app); + +// [Module] 택시 예상 비용 db 초기화 +initializeFareDatabase(); diff --git a/src/loadenv.ts b/src/loadenv.ts new file mode 100644 index 00000000..4e6f649b --- /dev/null +++ b/src/loadenv.ts @@ -0,0 +1,69 @@ +// 환경 변수에 따라 .env.production 또는 .env.development 파일을 읽어옵니다. +import dotenv from "dotenv"; +import type { ServiceAccount } from "firebase-admin"; +import type { Algorithm } from "jsonwebtoken"; + +if (process.env.NODE_ENV === undefined) { + // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. + // eslint-disable-next-line no-console + console.error("There is no NODE_ENV environment variable."); + process.exit(1); +} +dotenv.config({ path: `./.env.${process.env.NODE_ENV}` }); + +if (process.env.DB_PATH === undefined) { + // logger.ts가 아직 초기화되지 않았으므로 console.error를 사용합니다. + // eslint-disable-next-line no-console + console.error("There is no DB_PATH environment variable."); + process.exit(1); +} + +export const nodeEnv = process.env.NODE_ENV; // required ("production" or "development" or "test") +export const mongo = process.env.DB_PATH; // required +export const session = { + secret: process.env.SESSION_KEY || "TAXI_SESSION_KEY", // optional + expiry: 14 * 24 * 3600 * 1000, // 14일, ms 단위입니다. +}; +export const redis = process.env.REDIS_PATH; // optional +export const sparcssso = { + id: process.env.SPARCSSSO_CLIENT_ID || "", // optional + key: process.env.SPARCSSSO_CLIENT_KEY || "", // optional +}; +export const port = parseInt(process.env.PORT || "80", 10); // optional (default = 80) +export const corsWhiteList = (process.env.CORS_WHITELIST && + (JSON.parse(process.env.CORS_WHITELIST) as string[])) || [true]; // optional (default = [true]) +export const aws = { + accessKeyId: process.env.AWS_ACCESS_KEY_ID as string, // required + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string, // required + s3BucketName: process.env.AWS_S3_BUCKET_NAME as string, // required + s3Url: + process.env.AWS_S3_URL || + `https://${process.env.AWS_S3_BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com`, // optional +}; +export const jwt = { + secretKey: process.env.JWT_SECRET_KEY || "TAXI_JWT_KEY", // optional + option: { + algorithm: "HS256" as Algorithm, + // FIXME: remove FRONT_URL from issuer. 단, issuer를 변경하면 이전에 발급했던 모든 JWT가 무효화됩니다. + // See https://github.com/sparcs-kaist/taxi-back/issues/415 + issuer: process.env.FRONT_URL || "http://localhost:3000", // optional (default = "http://localhost:3000") + }, + TOKEN_EXPIRED: -3, + TOKEN_INVALID: -2, +}; +export const googleApplicationCredentials = + process.env.GOOGLE_APPLICATION_CREDENTIALS && + (JSON.parse(process.env.GOOGLE_APPLICATION_CREDENTIALS) as ServiceAccount); // optional +export const testAccounts = + (process.env.TEST_ACCOUNTS && + (JSON.parse(process.env.TEST_ACCOUNTS) as string[])) || + []; // optional +export const slackWebhookUrl = { + report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional +}; +// export const eventConfig = +// process.env.EVENT_CONFIG && JSON.parse(process.env.EVENT_CONFIG); // optional +export const naverMap = { + apiId: process.env.NAVER_MAP_API_ID || "", // optional + apiKey: process.env.NAVER_MAP_API_KEY || "", // optional +}; diff --git a/src/lottery/index.js b/src/lottery/index.js index d485dfe1..a00e3598 100644 --- a/src/lottery/index.js +++ b/src/lottery/index.js @@ -28,10 +28,10 @@ lotteryRouter.use(require("../middlewares/originValidator")); // [Router] APIs lotteryRouter.use("/globalState", require("./routes/globalState")); -lotteryRouter.use("/invite", require("./routes/invite")); +lotteryRouter.use("/invites", require("./routes/invites")); lotteryRouter.use("/transactions", require("./routes/transactions")); lotteryRouter.use("/items", require("./routes/items")); -lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); +// lotteryRouter.use("/publicNotice", require("./routes/publicNotice")); lotteryRouter.use("/quests", require("./routes/quests")); // [AdminJS] AdminJS에 표시할 Resource 생성 diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 72a62deb..6281bec7 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -13,96 +13,96 @@ const quests = buildQuests({ firstLogin: { name: "첫 발걸음", description: - "로그인만 해도 넙죽코인을 얻을 수 있다고?? 이벤트 기간에 처음으로 SPARCS Taxi 서비스에 로그인하여 넙죽코인을 받아보세요.", + "이벤트 참여만 해도 송편코인을 얻을 수 있다고?? 이벤트 참여에 동의하고 송편코인을 받아 보세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstLogin.png", - reward: 50, - }, - payingAndSending: { - name: "함께하는 택시의 여정", - description: - "2명 이상과 함께 택시를 타고 정산/송금까지 완료해보세요. 최대 3번까지 넙죽코인을 받을 수 있어요. 정산/송금 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", - imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_payingAndSending.png", - reward: 150, - maxCount: 0, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_firstLogin.png", + reward: 200, }, firstRoomCreation: { name: "첫 방 개설", description: - "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해보세요.", + "원하는 택시팟을 찾을 수 없다면? 원하는 조건으로 방 개설 페이지에서 방을 직접 개설해 보세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_firstRoomCreation.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_firstRoomCreation.png", + reward: 500, }, roomSharing: { - name: "너 T야? Taxi", + name: "이 택시팟은 진짜 유명한 택시팟임", description: - "방을 공유해 친구들을 택시에 초대해보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", + "방을 공유해 친구들을 택시팟에 초대해 보세요. 채팅창 상단의 햄버거(☰) 버튼을 누르면 공유하기 버튼을 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_roomSharing.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_roomSharing.png", + reward: 500, isApiRequired: true, }, - paying: { - name: "정산해요 택시의 숲", + fareSettlement: { + name: "정산의 신, 신팍스", description: - "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산하기를 요청해보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", + "2명 이상과 함께 택시를 타고 택시비를 결제한 후 정산을 요청해 보세요. 정산하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_paying.png", - reward: 100, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_fareSettlement.png", + reward: 2000, maxCount: 0, }, - sending: { + farePayment: { name: "송금 완료면 I am 신뢰에요", description: - "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 +버튼을 눌러 확인할 수 있어요.", + "2명 이상과 함께 택시를 타고 택시비를 결제한 분께 송금해 주세요. 송금하기 버튼은 채팅 페이지 좌측 하단의 + 버튼을 눌러 찾을 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_sending.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_farePayment.png", + reward: 2000, maxCount: 0, }, nicknameChanging: { name: "닉네임 폼 미쳤다", description: - "닉네임을 변경하여 자신을 표현하세요. 마이페이지의 수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", + "닉네임을 변경하여 자신을 표현하세요. 마이 페이지의 수정하기 버튼을 눌러 닉네임을 수정할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_nicknameChanging.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_nicknameChanging.png", + reward: 500, }, accountChanging: { name: "계좌 등록을 해야 능률이 올라갑니다", description: - "정산하기 기능을 더욱 빠르고 이용할 수 있다고? 계좌번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이페이지의 수정하기 버튼을 눌러 계좌번호를 등록 또는 수정할 수 있어요.", + "정산하기 기능을 더욱 빠르게 이용할 수 있다고? 계좌 번호를 등록하면 정산하기를 할 때 계좌가 자동으로 입력돼요. 마이 페이지의 수정하기 버튼을 눌러 계좌 번호를 등록 또는 수정할 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_accountChanging.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_accountChanging.png", + reward: 500, }, adPushAgreement: { name: "Taxi의 소울메이트", description: - "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 택시앱 사용자가 있을 때 알려드릴 수 있어요.", + "Taxi 서비스를 잊지 않도록 가끔 찾아갈게요! 광고성 푸시 알림 수신 동의를 해주시면 방이 많이 모이는 시즌, 주변에 Taxi 앱 사용자가 있을 때 알려드릴 수 있어요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_adPushAgreement.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_adPushAgreement.png", + reward: 500, }, eventSharing: { - name: "너 나랑 ㅌ태태택 (1명)", + name: "Taxi를 아십니까", description: - "내가 초대한 사람이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", + "내가 초대한 사람이 이벤트에 참여하면 송편코인을 드려요. 다른 사람의 초대를 받아 이벤트에 참여한 경우에도 이 퀘스트가 달성돼요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", - reward: 50, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_eventSharing.png", + reward: 700, maxCount: 0, }, - eventSharing5: { - name: "너 나랑 ㅌ태태택 (5명)", + dailyAttendance: { + name: "매일매일 출석 췤!", description: - "내가 초대한 사람이 5명이 Taxi에 가입하여 이벤트에 참여하면 넙죽코인을 드려요. 내가 초대한 사람도 넙죽코인을 받아요. 이벤트 안내 페이지의 이벤트 공유하기 버튼을 통해 카카오톡으로 초대 문자를 보낼 수 있어요!", + "매일 Taxi에 접속하면 하루 한 번 송편코인을 드려요! 하루에 한 번, 택시팟도 둘러보고 송편코인도 받아 가세요.", imageUrl: - "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024spring/quest_eventSharing.png", - reward: 250, - maxCount: 0, + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_dailyAttendance.png", + reward: 700, + maxCount: 17, + isApiRequired: true, + }, + itemPurchase: { + name: "Taxi에서 산 응모권", + description: + "응모권 교환소에서 아무 경품 응모권이나 구매해 보세요. Taxi에서 판매하는 응모권은 모두 정품이니 안심해도 좋아요.", + imageUrl: + "https://sparcs-taxi-prod.s3.ap-northeast-2.amazonaws.com/assets/event-2024fall/quest_itemPurchase.png", + reward: 500, }, }); @@ -111,40 +111,12 @@ const quests = buildQuests({ * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} - * @usage lottery/globalState/createUserGlobalStateHandler + * @usage lottery/globalState - createUserGlobalStateHandler */ const completeFirstLoginQuest = async (userId, timestamp) => { return await completeQuest(userId, timestamp, quests.firstLogin); }; -/** - * payingAndSending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. - * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. - * @param {Object} roomObject - 방의 정보입니다. - * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. - * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. - * @param {Date} roomObject.time - 출발 시각입니다. - * @returns {Promise} - * @description 정산 요청 또는 송금이 이루어질 때마다 호출해 주세요. - * @usage rooms - commitSettlementHandler, rooms - commitPaymentHandler - */ -const completePayingAndSendingQuest = async (userId, timestamp, roomObject) => { - logger.info( - `User ${userId} requested to complete payingAndSendingQuest in Room ${roomObject._id}` - ); - - if (roomObject.part.length < 2) return null; - if ( - !eventPeriod || - roomObject.time >= eventPeriod.endAt || - roomObject.time < eventPeriod.startAt - ) - return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - - return await completeQuest(userId, timestamp, quests.payingAndSending); -}; - /** * firstRoomCreation 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. @@ -158,7 +130,7 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { }; /** - * paying 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -169,9 +141,9 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { * @description 정산 요청이 이루어질 때마다 호출해 주세요. * @usage rooms - commitSettlementHandler */ -const completePayingQuest = async (userId, timestamp, roomObject) => { +const completeFareSettlementQuest = async (userId, timestamp, roomObject) => { logger.info( - `User ${userId} requested to complete payingQuest in Room ${roomObject._id}` + `User ${userId} requested to complete fareSettlementQuest in Room ${roomObject._id}` ); if (roomObject.part.length < 2) return null; @@ -182,11 +154,11 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.paying); + return await completeQuest(userId, timestamp, quests.fareSettlement); }; /** - * sending 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -197,9 +169,9 @@ const completePayingQuest = async (userId, timestamp, roomObject) => { * @description 송금이 이루어질 때마다 호출해 주세요. * @usage rooms - commitPaymentHandler */ -const completeSendingQuest = async (userId, timestamp, roomObject) => { +const completeFarePaymentQuest = async (userId, timestamp, roomObject) => { logger.info( - `User ${userId} requested to complete sendingQuest in Room ${roomObject._id}` + `User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}` ); if (roomObject.part.length < 2) return null; @@ -210,7 +182,7 @@ const completeSendingQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.sending); + return await completeQuest(userId, timestamp, quests.farePayment); }; /** @@ -241,13 +213,13 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { }; /** - * adPushAgreementQuest 퀘스트의 완료를 요청합니다. + * adPushAgreement 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. * @returns {Promise} * @description 알림 옵션을 변경할 때마다 호출해 주세요. - * @usage notifications/editOptionsHandler + * @usage notifications - editOptionsHandler */ const completeAdPushAgreementQuest = async ( userId, @@ -260,38 +232,36 @@ const completeAdPushAgreementQuest = async ( }; /** - * eventSharing, eventSharing5 퀘스트의 완료를 요청합니다. + * eventSharing 퀘스트의 완료를 요청합니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} - * @description 초대 링크를 통해 사용자가 이벤트에 참여할 때마다, 초대한 사용자 및 초대받은 사용자에 대해 각각 호출해 주세요. + * @usage lottery/globalState - createUserGlobalStateHandler */ const completeEventSharingQuest = async (userId, timestamp) => { - const eventSharingResult = await completeQuest( - userId, - timestamp, - quests.eventSharing - ); - if (!eventSharingResult || eventSharingResult.questCount % 5 !== 0) - return [eventSharingResult, null]; + return await completeQuest(userId, timestamp, quests.eventSharing); +}; - const eventSharing5Result = await completeQuest( - userId, - timestamp, - quests.eventSharing5 - ); - return [eventSharingResult, eventSharing5Result]; +/** + * itemPurchase 퀘스트의 완료를 요청합니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @returns {Promise} + * @description 상품을 구입할 때마다 호출해 주세요. + */ +const completeItemPurchaseQuest = async (userId, timestamp) => { + return await completeQuest(userId, timestamp, quests.itemPurchase); }; module.exports = { quests, completeFirstLoginQuest, - completePayingAndSendingQuest, completeFirstRoomCreationQuest, - completePayingQuest, - completeSendingQuest, + completeFareSettlementQuest, + completeFarePaymentQuest, completeNicknameChangingQuest, completeAccountChangingQuest, completeAdPushAgreementQuest, completeEventSharingQuest, + completeItemPurchaseQuest, }; diff --git a/src/lottery/modules/populates/transactions.js b/src/lottery/modules/populates/transactions.js index 6d965258..a09428c5 100644 --- a/src/lottery/modules/populates/transactions.js +++ b/src/lottery/modules/populates/transactions.js @@ -1,8 +1,7 @@ const transactionPopulateOption = [ { - path: "item", - select: - "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType", + path: "itemId", + select: "name imageUrl", }, ]; diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 04c6cd4c..6bf162fb 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -14,6 +14,7 @@ const eventPeriod = eventConfig && { }; const requiredQuestFields = ["name", "description", "imageUrl", "reward"]; + const buildQuests = (quests) => { for (const [id, quest] of Object.entries(quests)) { // quest에 필수 필드가 모두 포함되어 있는지 확인합니다. @@ -61,7 +62,7 @@ const buildQuests = (quests) => { * @param {number} quest.reward.credit - 퀘스트의 완료 보상 중 재화의 양입니다. * @param {number} quest.reward.ticket1 - 퀘스트의 완료 보상 중 일반 티켓의 개수입니다. * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. - * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화 된 경우에도 실패로 처리됩니다. + * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다. */ const completeQuest = async (userId, timestamp, quest) => { try { @@ -84,7 +85,7 @@ const completeQuest = async (userId, timestamp, quest) => { // 3단계: 유저의 퀘스트 완료 횟수를 확인합니다. // maxCount가 0인 경우, 무제한으로 퀘스트를 완료할 수 있습니다. const questCount = eventStatus.completedQuests.filter( - (completedQuestId) => completedQuestId === quest.id + ({ questId }) => questId === quest.id ).length; if (quest.maxCount > 0 && questCount >= quest.maxCount) { logger.info( @@ -118,7 +119,10 @@ const completeQuest = async (userId, timestamp, quest) => { ticket1Amount: quest.reward.ticket1, }, $push: { - completedQuests: quest.id, + completedQuests: { + questId: quest.id, + completedAt: timestamp, + }, }, } ); @@ -143,7 +147,7 @@ const completeQuest = async (userId, timestamp, quest) => { amount: 0, userId, questId: quest.id, - item: ticket1._id, + itemId: ticket1._id, comment: `"${quest.name}" 퀘스트를 완료해 "${ticket1.name}" ${quest.reward.ticket1}개를 획득했습니다.`, }); await transaction.save(); diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js index 600c99ec..09b6c80e 100644 --- a/src/lottery/modules/stores/mongo.js +++ b/src/lottery/modules/stores/mongo.js @@ -10,6 +10,17 @@ const integerValidator = { message: "{VALUE} is not an integer value", }; +const completedQuestSchema = Schema({ + questId: { + type: String, + required: true, + }, + completedAt: { + type: Date, + required: true, + }, +}); + const eventStatusSchema = Schema({ userId: { type: Schema.Types.ObjectId, @@ -17,7 +28,7 @@ const eventStatusSchema = Schema({ required: true, }, completedQuests: { - type: [String], + type: [completedQuestSchema], default: [], }, creditAmount: { @@ -42,17 +53,11 @@ const eventStatusSchema = Schema({ type: Boolean, default: false, }, - group: { - type: Number, - required: true, - min: 1, - validate: integerValidator, - }, // 소속된 새터반 inviter: { type: Schema.Types.ObjectId, ref: "User", }, // 이 사용자를 초대한 사용자 - isEnabledInviteUrl: { + isInviteUrlEnabled: { type: Boolean, default: false, }, // 초대 링크 활성화 여부 @@ -101,7 +106,13 @@ const itemSchema = Schema({ required: true, min: 0, validate: integerValidator, - }, + }, // 의미 없는 값, 기존 코드와의 호환성을 위해 남겨둡니다. + realStock: { + type: Number, + required: true, + min: 1, + validate: integerValidator, + }, // 상품의 실제 재고 itemType: { type: Number, enum: [0, 1, 2, 3], @@ -124,13 +135,13 @@ const transactionSchema = Schema({ type: String, enum: ["get", "use"], required: true, - }, + }, // get: 재화 획득, use: 재화 사용 amount: { type: Number, required: true, min: 0, validate: integerValidator, - }, + }, // 재화의 변화량의 절댓값 userId: { type: Schema.Types.ObjectId, ref: "User", @@ -138,22 +149,23 @@ const transactionSchema = Schema({ }, questId: { type: String, - }, - item: { + }, // 완료한 퀘스트의 ID + itemId: { type: Schema.Types.ObjectId, ref: `${modelNamePrefix}Item`, - }, - itemType: { + }, // 획득한 상품의 ID + itemAmount: { type: Number, - enum: [0, 1, 2, 3], - }, + min: 1, + validate: integerValidator, + }, // 획득한 상품의 개수 comment: { type: String, required: true, }, }); transactionSchema.set("timestamps", { - createdAt: "createAt", + createdAt: "createdAt", updatedAt: false, }); diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js index 4af3493e..44b62384 100644 --- a/src/lottery/routes/docs/globalState.js +++ b/src/lottery/routes/docs/globalState.js @@ -5,24 +5,21 @@ const globalStateDocs = {}; globalStateDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], - summary: "Frontend에서 Global state로 관리하는 정보 반환", + summary: "Frontend에서 Global State로 관리하는 정보 반환", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 가져옵니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리하는 정보를 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { type: "object", required: [ "isAgreeOnTermsOfEvent", - "isEligible", + "isBanned", "creditAmount", - "groupCreditAmount", - "completedQuests", - "group", "quests", + "completedQuests", ], properties: { isAgreeOnTermsOfEvent: { @@ -30,44 +27,19 @@ globalStateDocs[`${apiPrefix}/`] = { description: "유저의 이벤트 참여 동의 여부", example: true, }, - isEligible: { - type: "boolean", - description: "유저의 이벤트 참여 가능 여부", - example: true, - }, - creditAmount: { - type: "number", - description: "재화 개수. 0 이상입니다.", - example: 1000, - }, - groupCreditAmount: { - type: "number", - description: "소속 새터반에 소속된 유저의 전체 재화 개수", - example: 35000, - }, - completedQuests: { - type: "array", - description: - "유저가 완료한 퀘스트의 배열. 여러 번 완료할 수 있는 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", - items: { - type: "string", - description: "Quest의 Id", - example: "QUEST ID", - }, - }, isBanned: { type: "boolean", - description: "해당 유저 제재 대상 여부", + description: "유저의 이벤트 참여 제한 여부", example: false, }, - group: { + creditAmount: { type: "number", - description: "유저의 소속 새터반", - example: 16, + description: "유저의 재화 개수. 0 이상의 정수입니다.", + example: 1000, }, quests: { type: "array", - description: "Quest의 배열", + description: "전체 퀘스트의 배열", items: { type: "object", required: [ @@ -82,7 +54,7 @@ globalStateDocs[`${apiPrefix}/`] = { properties: { id: { type: "string", - description: "Quest의 Id", + description: "퀘스트의 Id", example: "QUEST ID", }, name: { @@ -98,34 +70,54 @@ globalStateDocs[`${apiPrefix}/`] = { }, imageUrl: { type: "string", - description: "이미지 썸네일 URL", + description: "퀘스트의 썸네일 이미지 URL", example: "THUMBNAIL URL", }, reward: { type: "object", - description: "완료 보상", required: ["credit"], properties: { credit: { type: "number", - description: "완료 보상 중 재화의 개수입니다.", + description: "퀘스트의 완료 보상 중 재화의 개수", example: 100, }, }, }, maxCount: { type: "number", - description: "최대 완료 가능 횟수", + description: "퀘스트의 최대 완료 가능 횟수", example: 1, }, isApiRequired: { type: "boolean", - description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청할 수 있는지 여부`, + description: `/events/${eventConfig?.mode}/quests/complete/:questId API를 통해 퀘스트 완료를 요청해야 하는지의 여부`, example: false, }, }, }, }, + completedQuests: { + type: "array", + description: + "유저가 완료한 퀘스트의 배열. 여러 번 완료한 퀘스트의 경우 배열 내에 같은 퀘스트가 여러 번 포함됩니다.", + items: { + type: "object", + required: ["questId", "completedAt"], + properties: { + questId: { + type: "string", + description: "퀘스트의 Id", + example: "QUEST ID", + }, + completedAt: { + type: "string", + description: "퀘스트의 완료 시각", + example: "2023-01-01 00:00:00", + }, + }, + }, + }, }, }, }, @@ -137,11 +129,10 @@ globalStateDocs[`${apiPrefix}/`] = { globalStateDocs[`${apiPrefix}/create`] = { post: { tags: [`${apiPrefix}`], - summary: "Frontend에서 Global state로 관리하는 정보 생성", + summary: "Frontend에서 Global State로 관리할 정보 생성", description: - "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global state로 관리할 정보를 생성합니다.", + "유저의 재화 개수, 퀘스트 완료 상태 등 Frontend에서 Global State로 관리할 정보를 생성합니다.", requestBody: { - description: "", content: { "application/json": { schema: { @@ -152,7 +143,6 @@ globalStateDocs[`${apiPrefix}/create`] = { }, responses: { 200: { - description: "", content: { "application/json": { schema: { diff --git a/src/lottery/routes/docs/invite.js b/src/lottery/routes/docs/invites.js similarity index 60% rename from src/lottery/routes/docs/invite.js rename to src/lottery/routes/docs/invites.js index 3a3972da..cfe37214 100644 --- a/src/lottery/routes/docs/invite.js +++ b/src/lottery/routes/docs/invites.js @@ -1,25 +1,23 @@ const { eventConfig } = require("../../../../loadenv"); -const apiPrefix = `/events/${eventConfig?.mode}/invite`; +const apiPrefix = `/events/${eventConfig?.mode}/invites`; -const inviteDocs = {}; -inviteDocs[`${apiPrefix}/search/:inviter`] = { +const invitesDocs = {}; +invitesDocs[`${apiPrefix}/search/{inviter}`] = { get: { tags: [`${apiPrefix}`], - summary: "초대자 정보 조회", - description: "초대자의 정보를 조회합니다.", - requestBody: { - description: "", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/searchInviterHandler", - }, - }, + summary: "초대한 유저의 정보 반환", + description: "초대한 유저의 정보를 가져옵니다.", + parameters: [ + { + in: "path", + name: "inviter", + required: true, + description: "초대한 유저의 eventStatus ObjectId", + example: "INVITER ID", }, - }, + ], responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -28,13 +26,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { properties: { nickname: { type: "string", - description: "초대자의 닉네임", - example: "asdf", + description: "초대한 유저의 닉네임", + example: "static", }, profileImageUrl: { type: "string", - description: "초대자의 프로필 이미지 URL", - example: "IMAGE URL", + description: "초대한 유저의 프로필 이미지 URL", + example: "PROFILE URL", }, }, }, @@ -44,14 +42,13 @@ inviteDocs[`${apiPrefix}/search/:inviter`] = { }, }, }; -inviteDocs[`${apiPrefix}/create`] = { +invitesDocs[`${apiPrefix}/create`] = { post: { tags: [`${apiPrefix}`], summary: "초대 링크 생성", description: "초대 링크를 생성합니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -72,4 +69,4 @@ inviteDocs[`${apiPrefix}/create`] = { }, }; -module.exports = inviteDocs; +module.exports = invitesDocs; diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js index b08aeaf7..28ecd53d 100644 --- a/src/lottery/routes/docs/items.js +++ b/src/lottery/routes/docs/items.js @@ -2,15 +2,13 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/items`; const itemsDocs = {}; -itemsDocs[`${apiPrefix}/list`] = { +itemsDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], - summary: "상점에서 판매하는 모든 상품의 목록 반환", - description: - "상점에서 판매하는 모든 상품의 목록을 가져옵니다. 매진된 상품도 가져옵니다.", + summary: "상점에서 판매하는 상품의 목록 반환", + description: "상점에서 판매하는 상품의 목록을 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -19,9 +17,62 @@ itemsDocs[`${apiPrefix}/list`] = { properties: { items: { type: "array", - description: "Item의 배열", + description: "상품의 배열", items: { - $ref: "#/components/schemas/item", + type: "object", + required: [ + "_id", + "name", + "description", + "imageUrl", + "price", + "isDisabled", + "itemType", + "realStock", + ], + properties: { + _id: { + type: "string", + description: "상품의 ObjectId", + example: "ITEM ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "진짜 송편", + }, + description: { + type: "string", + description: "상품의 설명", + example: "먹을 수 있는 송편입니다.", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "THUMBNAIL URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상의 정수입니다.", + example: 400, + }, + isDisabled: { + type: "boolean", + description: "상품의 판매 중지 여부", + example: false, + }, + itemType: { + type: "number", + description: + "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", + example: 0, + }, + realStock: { + type: "number", + description: "상품의 실제 재고", + example: 30, + }, + }, }, }, }, @@ -32,56 +83,230 @@ itemsDocs[`${apiPrefix}/list`] = { }, }, }; -itemsDocs[`${apiPrefix}/purchase/:itemId`] = { - post: { +itemsDocs[`${apiPrefix}/{itemId}`] = { + get: { tags: [`${apiPrefix}`], - summary: "상품 구매", - description: "상품을 구매합니다.", - requestBody: { - description: "", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/purchaseHandler", + summary: "상점에서 판매하는 특정 상품의 정보 반환", + description: "상점에서 판매하는 특정 상품의 정보를 가져옵니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "정보를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], + responses: { + 200: { + content: { + "application/json": { + schema: { + type: "object", + required: ["item"], + properties: { + item: { + type: "object", + required: [ + "_id", + "name", + "description", + "imageUrl", + "price", + "isDisabled", + "itemType", + "realStock", + ], + description: "상품의 정보", + properties: { + _id: { + type: "string", + description: "상품의 ObjectId", + example: "ITEM ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "진짜 송편", + }, + description: { + type: "string", + description: "상품의 설명", + example: "먹을 수 있는 송편입니다.", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "THUMBNAIL URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상의 정수입니다.", + example: 400, + }, + isDisabled: { + type: "boolean", + description: "상품의 판매 중지 여부", + example: false, + }, + itemType: { + type: "number", + description: + "상품의 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", + example: 0, + }, + realStock: { + type: "number", + description: "상품의 실제 재고", + example: 30, + }, + }, + }, + }, + }, }, }, }, }, + }, +}; +itemsDocs[`${apiPrefix}/leaderboard/{itemId}`] = { + get: { + tags: [`${apiPrefix}`], + summary: "상품 리더보드 반환", + description: "상품 리더보드를 가져옵니다. 일반 상품만 리더보드를 갖습니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "리더보드를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], responses: { 200: { - description: "", content: { "application/json": { schema: { type: "object", - required: ["result"], + required: ["leaderboard", "totalAmount", "totalUser"], properties: { - result: { - type: "boolean", - description: "성공 여부. 항상 true입니다.", - example: true, + leaderboard: { + type: "array", + description: "상품 리더보드. 상위 20등까지만 반환됩니다.", + items: { + type: "object", + required: [ + "nickname", + "profileImageUrl", + "amount", + "probability", + "rank", + ], + properties: { + nickname: { + type: "string", + description: "유저의 닉네임", + example: "static", + }, + profileImageUrl: { + type: "string", + description: "유저의 프로필 이미지 URL", + example: "PROFILE URL", + }, + amount: { + type: "number", + description: "유저가 상품을 구입한 횟수", + example: 3, + }, + probability: { + type: "number", + description: "유저가 상품에 당첨될 확률", + example: 0.1, + }, + rank: { + type: "number", + description: "순위", + example: 1, + }, + }, + }, + }, + totalAmount: { + type: "number", + description: "상품의 총 판매량", + example: 100, + }, + totalUser: { + type: "number", + description: "상품을 구입한 유저의 수", + example: 50, + }, + rank: { + type: "number", + description: "현재 유저의 리더보드 순위. 1부터 시작합니다.", + example: 1, }, - reward: { - $ref: "#/components/schemas/rewardItem", + amount: { + type: "number", + description: "현재 유저가 상품을 구입한 횟수", + example: 3, + }, + probability: { + type: "number", + description: "현재 유저가 상품에 당첨될 확률", + example: 0.1, }, }, }, }, }, }, - 400: { - description: - "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.", + }, + }, +}; +itemsDocs[`${apiPrefix}/purchase/{itemId}`] = { + post: { + tags: [`${apiPrefix}`], + summary: "상품 구입", + description: "상품을 구입합니다.", + parameters: [ + { + in: "path", + name: "itemId", + required: true, + description: "리더보드를 조회할 상품의 ObjectId", + example: "ITEM ID", + }, + ], + requestBody: { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/purchaseItemHandlerBody", + }, + }, + }, + }, + responses: { + 200: { content: { "application/json": { schema: { type: "object", - required: ["error"], + required: ["result"], properties: { - error: { - type: "string", - description: "", - example: "checkBanned: banned user", + result: { + type: "boolean", + description: "성공 여부. 항상 true입니다.", + example: true, + }, + isJackpot: { + type: "boolean", + description: + "대박 여부. 랜덤박스를 구입한 경우에만 포함됩니다.", + example: true, }, }, }, diff --git a/src/lottery/routes/docs/publicNotice.js b/src/lottery/routes/docs/publicNotice.js index 23a410b2..bcf2cc78 100644 --- a/src/lottery/routes/docs/publicNotice.js +++ b/src/lottery/routes/docs/publicNotice.js @@ -2,7 +2,7 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/publicNotice`; const publicNoticeDocs = {}; -// 다음 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// 다음 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다. // // publicNoticeDocs[`${apiPrefix}/recentTransactions`] = { // get: { @@ -35,75 +35,75 @@ const publicNoticeDocs = {}; // }, // }, // }; -publicNoticeDocs[`${apiPrefix}/leaderboard`] = { - get: { - tags: [`${apiPrefix}`], - summary: "리더보드 반환", - description: - "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", - responses: { - 200: { - description: "", - content: { - "application/json": { - schema: { - type: "object", - required: ["leaderboard"], - properties: { - leaderboard: { - type: "array", - description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", - items: { - type: "object", - required: [ - "group", - "creditAmount", - "mvpNickname", - "mvpProfileImageUrl", - ], - properties: { - group: { - type: "number", - description: "새터반", - example: 16, - }, - creditAmount: { - type: "number", - description: "새터반에 소속된 유저의 전체 재화 개수", - example: 3000, - }, - mvpNickname: { - type: "string", - description: - "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", - example: "asdf", - }, - mvpProfileImageUrl: { - type: "string", - description: "MVP의 프로필 이미지 URL", - example: "IMAGE URL", - }, - }, - }, - }, - group: { - type: "number", - description: "유저의 소속 새터반", - example: 16, - }, - rank: { - type: "number", - description: - "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", - example: 1, - }, - }, - }, - }, - }, - }, - }, - }, -}; +// publicNoticeDocs[`${apiPrefix}/leaderboard`] = { +// get: { +// tags: [`${apiPrefix}`], +// summary: "리더보드 반환", +// description: +// "새터반 별 재화 개수 기준의 리더보드와 관련된 정보를 가져옵니다.", +// responses: { +// 200: { +// description: "", +// content: { +// "application/json": { +// schema: { +// type: "object", +// required: ["leaderboard"], +// properties: { +// leaderboard: { +// type: "array", +// description: "이벤트에 참여한 새터반 전체가 포함된 리더보드", +// items: { +// type: "object", +// required: [ +// "group", +// "creditAmount", +// "mvpNickname", +// "mvpProfileImageUrl", +// ], +// properties: { +// group: { +// type: "number", +// description: "새터반", +// example: 16, +// }, +// creditAmount: { +// type: "number", +// description: "새터반에 소속된 유저의 전체 재화 개수", +// example: 3000, +// }, +// mvpNickname: { +// type: "string", +// description: +// "MVP(새터반 내에서 가장 많은 재화를 가진 유저)의 닉네임", +// example: "asdf", +// }, +// mvpProfileImageUrl: { +// type: "string", +// description: "MVP의 프로필 이미지 URL", +// example: "IMAGE URL", +// }, +// }, +// }, +// }, +// group: { +// type: "number", +// description: "유저의 소속 새터반", +// example: 16, +// }, +// rank: { +// type: "number", +// description: +// "유저의 소속 새터반의 리더보드 순위. 1부터 시작합니다.", +// example: 1, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }; module.exports = publicNoticeDocs; diff --git a/src/lottery/routes/docs/quests.js b/src/lottery/routes/docs/quests.js index 14694f3e..42a9c022 100644 --- a/src/lottery/routes/docs/quests.js +++ b/src/lottery/routes/docs/quests.js @@ -2,24 +2,22 @@ const { eventConfig } = require("../../../../loadenv"); const apiPrefix = `/events/${eventConfig?.mode}/quests`; const questsDocs = {}; -questsDocs[`${apiPrefix}/complete/:questId`] = { +questsDocs[`${apiPrefix}/complete/{questId}`] = { post: { tags: [`${apiPrefix}`], summary: "퀘스트 완료 요청", description: "퀘스트의 완료를 요청합니다.", - requestBody: { - description: "", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/completeHandler", - }, - }, + parameters: [ + { + in: "path", + name: "questId", + required: true, + description: "완료를 요청할 퀘스트의 ID", + example: "QUEST ID", }, - }, + ], responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -36,25 +34,6 @@ questsDocs[`${apiPrefix}/complete/:questId`] = { }, }, }, - 400: { - description: - "checkBanned에서 이벤트에 동의하지 않은 사람과 제재 대상을 선별합니다.", - content: { - "application/json": { - schema: { - type: "object", - required: ["error"], - properties: { - error: { - type: "string", - description: "", - example: "checkBanned: banned user", - }, - }, - }, - }, - }, - }, }, }, }; diff --git a/src/lottery/routes/docs/schemas/globalStateSchema.js b/src/lottery/routes/docs/schemas/globalStateSchema.js index 0c3c55e6..26fe4ed9 100644 --- a/src/lottery/routes/docs/schemas/globalStateSchema.js +++ b/src/lottery/routes/docs/schemas/globalStateSchema.js @@ -1,12 +1,11 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); -const { objectId, user } = require("../../../../modules/patterns"); +const { objectId, user } = require("../../../../modules/patterns").default; const globalStateZod = { createUserGlobalStateHandler: z .object({ phoneNumber: z.string().regex(user.phoneNumber), - group: z.number().gte(1).lte(26), inviter: z.string().regex(objectId), }) .partial({ inviter: true }), diff --git a/src/lottery/routes/docs/schemas/inviteSchema.js b/src/lottery/routes/docs/schemas/inviteSchema.js deleted file mode 100644 index e3016557..00000000 --- a/src/lottery/routes/docs/schemas/inviteSchema.js +++ /dev/null @@ -1,13 +0,0 @@ -const { z } = require("zod"); -const { zodToSchemaObject } = require("../../../../routes/docs/utils"); -const { objectId } = require("../../../../modules/patterns"); - -const inviteZod = { - searchInviterHandler: z.object({ - inviter: z.string().regex(objectId), - }), -}; - -const inviteSchema = zodToSchemaObject(inviteZod); - -module.exports = { inviteSchema, inviteZod }; diff --git a/src/lottery/routes/docs/schemas/invitesSchema.js b/src/lottery/routes/docs/schemas/invitesSchema.js new file mode 100644 index 00000000..d43498cc --- /dev/null +++ b/src/lottery/routes/docs/schemas/invitesSchema.js @@ -0,0 +1,13 @@ +const { z } = require("zod"); +const { zodToSchemaObject } = require("../../../../routes/docs/utils"); +const { objectId } = require("../../../../modules/patterns").default; + +const invitesZod = { + searchInviterHandler: z.object({ + inviter: z.string().regex(objectId), + }), +}; + +const invitesSchema = zodToSchemaObject(invitesZod); + +module.exports = { invitesZod, invitesSchema }; diff --git a/src/lottery/routes/docs/schemas/itemsSchema.js b/src/lottery/routes/docs/schemas/itemsSchema.js index 80912cfb..68061d35 100644 --- a/src/lottery/routes/docs/schemas/itemsSchema.js +++ b/src/lottery/routes/docs/schemas/itemsSchema.js @@ -1,98 +1,22 @@ -/* Item에 대한 기본적인 프로퍼티를 갖고 있는 스키마입니다. - * TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. - */ -const itemBase = { - type: "object", - required: [ - "_id", - "name", - "imageUrl", - "price", - "description", - "isDisabled", - "stock", - ], - properties: { - _id: { - type: "string", - description: "Item의 ObjectId", - example: "OBJECT ID", - }, - name: { - type: "string", - description: "상품의 이름", - example: "진짜송편", - }, - imageUrl: { - type: "string", - description: "이미지 썸네일 URL", - example: "THUMBNAIL URL", - }, - instagramStoryStickerImageUrl: { - type: "string", - description: "인스타그램 스토리 스티커 이미지 URL", - example: "STICKER URL", - }, - price: { - type: "number", - description: "상품의 가격. 0 이상입니다.", - example: 400, - }, - description: { - type: "string", - description: "상품의 설명", - example: "맛있는 송편입니다.", - }, - isDisabled: { - type: "boolean", - description: "판매 중지 여부", - example: false, - }, - stock: { - type: "number", - description: "남은 상품 재고. 재고가 있는 경우 1, 없는 경우 0입니다.", - example: 1, - }, - }, -}; +const { z } = require("zod"); +const { zodToSchemaObject } = require("../../../../routes/docs/utils"); +const { objectId } = require("../../../../modules/patterns").default; -/** itemBase에 itemType(상품 유형) 프로퍼티가 추가된 스키마입니다. */ -const itemWithType = { - type: itemBase.type, - required: itemBase.required.concat(["itemType"]), - properties: { - ...itemBase.properties, - itemType: { - type: "number", - description: - "상품 유형. 0: 일반 상품, 1: 일반 티켓, 2: 고급 티켓, 3: 랜덤박스입니다.", - example: 0, - }, - }, +const itemsZod = { + getItemHandler: z.object({ + itemId: z.string().regex(objectId), + }), + getItemLeaderboardHandler: z.object({ + itemId: z.string().regex(objectId), + }), + purchaseItemHandlerParams: z.object({ + itemId: z.string().regex(objectId), + }), + purchaseItemHandlerBody: z.object({ + amount: z.number().int().positive(), + }), }; -const itemsSchema = { - item: itemWithType, - relatedItem: { - ...itemWithType, - description: - "Transaction과 관련된 아이템의 Object. 아이템과 관련된 Transaction인 경우에만 포함됩니다.", - }, - rewardItem: { - ...itemBase, - description: "랜덤박스를 구입한 경우에만 포함됩니다.", - }, - purchaseHandler: { - type: "object", - required: ["itemId"], - properties: { - itemId: { - type: "string", - pattern: "^[a-fA-F\\d]{24}$", - }, - }, - errorMessage: "validation: bad request", - }, -}; +const itemsSchema = zodToSchemaObject(itemsZod); -module.exports = itemsSchema; +module.exports = { itemsZod, itemsSchema }; diff --git a/src/lottery/routes/docs/schemas/questsSchema.js b/src/lottery/routes/docs/schemas/questsSchema.js index 2efd11cd..8daf560d 100644 --- a/src/lottery/routes/docs/schemas/questsSchema.js +++ b/src/lottery/routes/docs/schemas/questsSchema.js @@ -2,7 +2,9 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../../../../routes/docs/utils"); const questsZod = { - completeHandler: z.object({ questId: z.enum(["roomSharing"]) }), + completeQuestHandler: z.object({ + questId: z.enum(["roomSharing", "dailyAttendance"]), + }), }; const questsSchema = zodToSchemaObject(questsZod); diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js index 38ca8298..0b6702da 100644 --- a/src/lottery/routes/docs/swaggerDocs.js +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -1,13 +1,13 @@ const globalStateDocs = require("./globalState"); -const inviteDocs = require("./invite"); +const invitesDocs = require("./invites"); const itemsDocs = require("./items"); const publicNoticeDocs = require("./publicNotice"); const questsDocs = require("./quests"); const transactionsDocs = require("./transactions"); const { globalStateSchema } = require("./schemas/globalStateSchema"); -const { inviteSchema } = require("./schemas/inviteSchema"); -const itemsSchema = require("./schemas/itemsSchema"); +const { invitesSchema } = require("./schemas/invitesSchema"); +const { itemsSchema } = require("./schemas/itemsSchema"); const { questsSchema } = require("./schemas/questsSchema"); const { eventConfig } = require("../../../../loadenv"); @@ -20,41 +20,39 @@ const eventSwaggerDocs = { description: "이벤트 - Global State 관련 API", }, { - name: `${apiPrefix}/invite`, + name: `${apiPrefix}/invites`, description: "이벤트 - 초대 링크 관련 API", }, - // 이 태그는 2024 봄학기 이벤트에서 사용되지 않습니다. - // - // { - // name: `${apiPrefix}/items`, - // description: "이벤트 - 아이템 관련 API", - // }, { - name: `${apiPrefix}/publicNotice`, - description: "이벤트 - 아이템 구매, 뽑기, 획득 공지 관련 API", + name: `${apiPrefix}/items`, + description: "이벤트 - 상품 관련 API", }, + // { + // name: `${apiPrefix}/publicNotice`, + // description: "이벤트 - 상품 구매, 뽑기, 획득 공지 관련 API", + // }, { name: `${apiPrefix}/quests`, description: "이벤트 - 퀘스트 관련 API", }, { name: `${apiPrefix}/transactions`, - description: "이벤트 - 입출금 내역 관련 API", + description: "이벤트 - 재화 입출금 내역 관련 API", }, ], paths: { ...globalStateDocs, - ...inviteDocs, - //...itemsDocs, - ...publicNoticeDocs, + ...invitesDocs, + ...itemsDocs, + // ...publicNoticeDocs, ...questsDocs, ...transactionsDocs, }, components: { schemas: { ...globalStateSchema, - ...inviteSchema, - //...itemsSchema, + ...invitesSchema, + ...itemsSchema, ...questsSchema, }, }, diff --git a/src/lottery/routes/docs/transactions.js b/src/lottery/routes/docs/transactions.js index fa78238b..a041b949 100644 --- a/src/lottery/routes/docs/transactions.js +++ b/src/lottery/routes/docs/transactions.js @@ -6,10 +6,9 @@ transactionsDocs[`${apiPrefix}/`] = { get: { tags: [`${apiPrefix}`], summary: "재화 입출금 내역 반환", - description: "유저의 재화 입출금 내역을 가져옵니다.", + description: "재화 입출금 내역을 가져옵니다.", responses: { 200: { - description: "", content: { "application/json": { schema: { @@ -18,16 +17,11 @@ transactionsDocs[`${apiPrefix}/`] = { properties: { transactions: { type: "array", - description: "유저의 재화 입출금 기록의 배열", + description: "유저의 재화 입출금 내역의 배열", items: { type: "object", - required: ["_id", "type", "amount", "comment", "createAt"], + required: ["type", "amount", "comment", "createdAt"], properties: { - _id: { - type: "string", - description: "Transaction의 ObjectId", - example: "OBJECT ID", - }, type: { type: "string", description: @@ -41,18 +35,33 @@ transactionsDocs[`${apiPrefix}/`] = { }, questId: { type: "string", - description: - "Transaction과 관련된 퀘스트의 Id. 퀘스트와 관련된 Transaction인 경우에만 포함됩니다.", + description: "입출금 내역과 관련된 퀘스트의 Id", example: "QUEST ID", }, + item: { + type: "object", + required: ["name", "imageUrl"], + properties: { + name: { + type: "string", + description: "상품의 이름", + example: "랜덤 상자", + }, + imageUrl: { + type: "string", + description: "상품의 썸네일 이미지 URL", + example: "IMAGE URL", + }, + }, + }, comment: { type: "string", description: "입출금 내역에 대한 설명", example: "랜덤 상자 구입 - 50개 차감", }, - createAt: { + createdAt: { type: "string", - description: "입출금이 일어난 시각", + description: "입출금 내역이 생성된 시각", example: "2023-01-01 00:00:00", }, }, diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js index 1f2b4327..c4f37d39 100644 --- a/src/lottery/routes/globalState.js +++ b/src/lottery/routes/globalState.js @@ -1,7 +1,8 @@ const express = require("express"); +const router = express.Router(); + const { validateBody } = require("../../middlewares/zod"); const { globalStateZod } = require("./docs/schemas/globalStateSchema"); -const router = express.Router(); const globalStateHandlers = require("../services/globalState"); router.get("/", globalStateHandlers.getUserGlobalStateHandler); diff --git a/src/lottery/routes/invite.js b/src/lottery/routes/invite.js deleted file mode 100644 index eafa09cb..00000000 --- a/src/lottery/routes/invite.js +++ /dev/null @@ -1,20 +0,0 @@ -const express = require("express"); -const { validateParams } = require("../../middlewares/zod"); -const { inviteZod } = require("./docs/schemas/inviteSchema"); -const router = express.Router(); -const inviteHandlers = require("../services/invite"); - -router.get( - "/search/:inviter", - validateParams(inviteZod.searchInviterHandler), - inviteHandlers.searchInviterHandler -); - -// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 -router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); -router.use(require("../middlewares/timestampValidator")); - -router.post("/create", inviteHandlers.createInviteUrlHandler); - -module.exports = router; diff --git a/src/lottery/routes/invites.js b/src/lottery/routes/invites.js new file mode 100644 index 00000000..65e4271e --- /dev/null +++ b/src/lottery/routes/invites.js @@ -0,0 +1,21 @@ +const express = require("express"); +const router = express.Router(); + +const { validateParams } = require("../../middlewares/zod"); +const { invitesZod } = require("./docs/schemas/invitesSchema"); +const invitesHandlers = require("../services/invites"); + +router.get( + "/search/:inviter", + validateParams(invitesZod.searchInviterHandler), + invitesHandlers.searchInviterHandler +); + +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); + +router.post("/create", invitesHandlers.createInviteUrlHandler); + +module.exports = router; diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 5cdf98a8..0de8be18 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -1,23 +1,32 @@ const express = require("express"); - const router = express.Router(); -// TODO: 추후 코드 재사용시 상황에 맞춰 zod로 이전이 필요합니다. + +const { validateBody, validateParams } = require("../../middlewares/zod"); +const { itemsZod } = require("./docs/schemas/itemsSchema"); const itemsHandlers = require("../services/items"); -const itemsSchema = require("./docs/schemas/itemsSchema"); -// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. -// -// router.get("/list", itemsHandlers.listHandler); +router.get("/", itemsHandlers.getItemsHandler); +router.get( + "/:itemId", + validateParams(itemsZod.getItemHandler), + itemsHandlers.getItemHandler +); +router.get( + "/leaderboard/:itemId", + validateParams(itemsZod.getItemLeaderboardHandler), + itemsHandlers.getItemLeaderboardHandler +); -// // 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 -// router.use(require("../../middlewares/auth")); -// router.use(require("../middlewares/checkBanned")); -// router.use(require("../middlewares/timestampValidator")); +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 +router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/checkBanned")); +router.use(require("../middlewares/timestampValidator")); -// router.post( -// "/purchase/:itemId", -// validateParams(itemsSchema.purchaseHandler), -// itemsHandlers.purchaseHandler -// ); +router.post( + "/purchase/:itemId", + validateParams(itemsZod.purchaseItemHandlerParams), + validateBody(itemsZod.purchaseItemHandlerBody), + itemsHandlers.purchaseItemHandler +); module.exports = router; diff --git a/src/lottery/routes/publicNotice.js b/src/lottery/routes/publicNotice.js index 4698a193..f6646061 100644 --- a/src/lottery/routes/publicNotice.js +++ b/src/lottery/routes/publicNotice.js @@ -3,10 +3,10 @@ const express = require("express"); const router = express.Router(); const publicNoticeHandlers = require("../services/publicNotice"); -router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); - -// 아래의 Endpoint는 2024 봄학기 이벤트에서 사용되지 않습니다. +// 아래의 Endpoint들은 2024 추석 이벤트에서 사용되지 않습니다. // +// router.get("/leaderboard", publicNoticeHandlers.getGroupLeaderboardHandler); + // router.get( // "/recentTransactions", // publicNoticeHandlers.getRecentPurchaceItemListHandler diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js index 4941c8d2..e9845434 100644 --- a/src/lottery/routes/quests.js +++ b/src/lottery/routes/quests.js @@ -1,18 +1,19 @@ const express = require("express"); +const router = express.Router(); + const { validateParams } = require("../../middlewares/zod"); const { questsZod } = require("./docs/schemas/questsSchema"); -const router = express.Router(); const questsHandlers = require("../services/quests"); -// 아래의 Endpoint 접근 시 로그인, 차단 여부 체크 및 시각 체크 필요 +// 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 router.use(require("../../middlewares/auth")); router.use(require("../middlewares/checkBanned")); router.use(require("../middlewares/timestampValidator")); router.post( "/complete/:questId", - validateParams(questsZod.completeHandler), - questsHandlers.completeHandler + validateParams(questsZod.completeQuestHandler), + questsHandlers.completeQuestHandler ); module.exports = router; diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js index aee05d90..f9e375ca 100644 --- a/src/lottery/routes/transactions.js +++ b/src/lottery/routes/transactions.js @@ -1,6 +1,6 @@ const express = require("express"); - const router = express.Router(); + const transactionsHandlers = require("../services/transactions"); // 아래의 Endpoint 접근 시 로그인 필요 diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 1dfdb10c..7a16d0e7 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -8,63 +8,57 @@ const { eventConfig } = require("../../../loadenv"); const contracts = require("../modules/contracts"); const quests = Object.values(contracts.quests); -// 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. -const checkIsUserEligible = (user) => { - // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. - if (nodeEnv !== "production") return true; +// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. +// +// // 유저가 이벤트에 참여할 수 있는지 확인하는 함수입니다. +// const checkIsUserEligible = (user) => { +// // production 환경이 아닌 경우 테스트를 위해 참여 조건을 확인하지 않습니다. +// if (nodeEnv !== "production") return true; - const kaistId = parseInt(user?.subinfo?.kaist || "0"); - return 20240001 <= kaistId && kaistId <= 20241500; -}; +// const kaistId = parseInt(user?.subinfo?.kaist || "0"); +// return 20240001 <= kaistId && kaistId <= 20241500; +// }; const getUserGlobalStateHandler = async (req, res) => { try { const userId = isLogin(req) ? getLoginInfo(req).oid : null; - const user = - userId && - (await userModel.findOne({ _id: userId, withdraw: false }).lean()); - const eventStatus = userId && (await eventStatusModel - .findOne({ userId }, "completedQuests creditAmount isBanned group") + .findOne({ userId }, "completedQuests creditAmount isBanned") .lean()); if (!eventStatus) return res.json({ isAgreeOnTermsOfEvent: false, - isEligible: checkIsUserEligible(user) || !!user?.isAdmin, // 테스트를 위해 관리자인 경우 true로 설정합니다. 하지만 관리자이더라도 이벤트에 참여할 수 없습니다. - completedQuests: [], + isBanned: false, creditAmount: 0, - group: 0, - groupCreditAmount: 0, quests, + completedQuests: [], }); // group이 eventStatus.group과 같은 사용자들의 creditAmount를 합산합니다. - const groupCreditAmount = await eventStatusModel.aggregate([ - { - $match: { - group: eventStatus.group, - }, - }, - { - $group: { - _id: null, - creditAmount: { $sum: "$creditAmount" }, - }, - }, - ]); - const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; - if (!groupCreditAmountReal && groupCreditAmountReal !== 0) - return res - .status(500) - .json({ error: "GlobalState/ : internal server error" }); + // const groupCreditAmount = await eventStatusModel.aggregate([ + // { + // $match: { + // group: eventStatus.group, + // }, + // }, + // { + // $group: { + // _id: null, + // creditAmount: { $sum: "$creditAmount" }, + // }, + // }, + // ]); + // const groupCreditAmountReal = groupCreditAmount?.[0].creditAmount; + // if (!groupCreditAmountReal && groupCreditAmountReal !== 0) + // return res + // .status(500) + // .json({ error: "GlobalState/ : internal server error" }); return res.json({ - isAgreeOnTermsOfEvent: true, - isEligible: true, ...eventStatus, - groupCreditAmount: groupCreditAmountReal, + isAgreeOnTermsOfEvent: true, quests, }); } catch (err) { @@ -81,7 +75,7 @@ const createUserGlobalStateHandler = async (req, res) => { if (eventStatus) return res .status(400) - .json({ error: "GlobalState/Create : already created" }); + .json({ error: "GlobalState/create : already created" }); /* Request의 inviter 필드가 설정되어 있는데, 1. 해당되는 유저가 이벤트에 참여하지 않았거나, @@ -90,47 +84,53 @@ const createUserGlobalStateHandler = async (req, res) => { 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ const inviterStatus = req.body.inviter && - (await eventStatusModel.findOne({ _id: req.body.inviter }).lean()); + (await eventStatusModel.findById(req.body.inviter).lean()); if ( req.body.inviter && (!inviterStatus || inviterStatus.isBanned || - !inviterStatus.isEnabledInviteUrl) + !inviterStatus.isInviteUrlEnabled) ) return res.status(400).json({ - error: "GlobalState/Create : inviter did not participate in the event", + error: "GlobalState/create : invalid inviter", }); const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) return res .status(500) - .json({ error: "GlobalState/Create : internal server error" }); + .json({ error: "GlobalState/create : internal server error" }); // 유저가 이벤트에 참여할 수 있는지 확인합니다. - const isEligible = checkIsUserEligible(user); - if (!isEligible) - return res.status(400).json({ - error: "GlobalState/Create : not eligible to participate in the event", - }); - - // 수집한 전화번호를 User Document에 저장합니다. - // 다른 이벤트 참여 과정에서 문제가 생길 수 있으므로, 이벤트 참여 자격이 있는 경우에만 저장합니다. - user.phoneNumber = req.body.phoneNumber; - await user.save(); + // const isEligible = checkIsUserEligible(user); + // if (!isEligible) + // return res.status(400).json({ + // error: "GlobalState/create : not eligible to participate in the event", + // }); + + // 필요한 경우 유저의 전화번호를 업데이트합니다. + if (user.phoneNumber !== req.body.phoneNumber) { + if (user.phoneNumber) { + logger.info(`Past user phone number: ${user.phoneNumber}`); + logger.info(`Update user phone number: ${req.body.phoneNumber}`); + } + + user.phoneNumber = req.body.phoneNumber; + await user.save(); + } // EventStatus Document를 생성합니다. eventStatus = new eventStatusModel({ userId: req.userOid, creditAmount: eventConfig?.credit.initialAmount ?? 0, - group: req.body.group, - inviter: req.body.inviter, + inviter: inviterStatus?.userId ?? undefined, }); await eventStatus.save(); + // 퀘스트를 완료 처리합니다. await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); - if (req.body.inviter) { + if (inviterStatus) { await contracts.completeEventSharingQuest(req.userOid, req.timestamp); await contracts.completeEventSharingQuest( inviterStatus.userId, @@ -143,7 +143,7 @@ const createUserGlobalStateHandler = async (req, res) => { logger.error(err); res .status(500) - .json({ error: "GlobalState/Create : internal server error" }); + .json({ error: "GlobalState/create : internal server error" }); } }; diff --git a/src/lottery/services/invite.js b/src/lottery/services/invite.js deleted file mode 100644 index ffe219f1..00000000 --- a/src/lottery/services/invite.js +++ /dev/null @@ -1,69 +0,0 @@ -const { eventStatusModel } = require("../modules/stores/mongo"); -const { userModel } = require("../../modules/stores/mongo"); -const logger = require("../../modules/logger"); - -const { eventConfig } = require("../../../loadenv"); - -const searchInviterHandler = async (req, res) => { - try { - const { inviter } = req.params; - const inviterStatus = await eventStatusModel.findOne({ _id: inviter }); - if ( - !inviterStatus || - !inviterStatus.isEnabledInviteUrl || - inviterStatus.isBanned - ) - return res.status(400).json({ error: "Invite/Search : invalid inviter" }); - - const inviterInfo = await userModel.findOne({ - _id: inviterStatus.userId, - withdraw: false, - }); - if (!inviterInfo) - return res - .status(500) - .json({ error: "Invite/Search : internal server error" }); - - return res.json({ - nickname: inviterInfo.nickname, - profileImageUrl: inviterInfo.profileImageUrl, - }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Invite/Search : internal server error" }); - } -}; - -const createInviteUrlHandler = async (req, res) => { - try { - const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; - - if (req.eventStatus.isEnabledInviteUrl) return res.json({ inviteUrl }); - - const eventStatus = await eventStatusModel - .findOneAndUpdate( - { - _id: req.eventStatus._id, - isEnabledInviteUrl: false, - }, - { - isEnabledInviteUrl: true, - } - ) - .lean(); - if (!eventStatus) - return res - .status(500) - .json({ error: "Invite/Create : internal server error" }); - - return res.json({ inviteUrl }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Invite/Create : internal server error" }); - } -}; - -module.exports = { - searchInviterHandler, - createInviteUrlHandler, -}; diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js new file mode 100644 index 00000000..935e158e --- /dev/null +++ b/src/lottery/services/invites.js @@ -0,0 +1,76 @@ +const { eventStatusModel } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +const { eventConfig } = require("../../../loadenv"); + +const searchInviterHandler = async (req, res) => { + try { + /* 1. 해당되는 유저가 이벤트에 참여하지 않았거나, + 2. 해당되는 유저의 이벤트 참여가 제한된 상태이거나, + 3. 해당되는 유저의 초대 링크가 활성화되지 않았으면, + 에러를 발생시킵니다. 개인정보 보호를 위해 오류 메세지는 하나로 통일하였습니다. */ + const inviterStatus = await eventStatusModel + .findById(req.params.inviter) + .lean(); + if ( + !inviterStatus || + inviterStatus.isBanned || + !inviterStatus.isInviteUrlEnabled + ) + return res + .status(400) + .json({ error: "Invites/search : invalid inviter" }); + + // 해당되는 유저의 닉네임과 프로필 이미지를 가져옵니다. + const inviter = await userModel + .findOne( + { _id: inviterStatus.userId, withdraw: false }, + "nickname profileImageUrl" + ) + .lean(); + if (!inviter) + return res + .status(500) + .json({ error: "Invites/search : internal server error" }); + + return res.json(inviter); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invites/search : internal server error" }); + } +}; + +const createInviteUrlHandler = async (req, res) => { + try { + const inviteUrl = `${req.origin}/event/${eventConfig?.mode}-invite/${req.eventStatus._id}`; + + // 이미 초대 링크가 활성화된 경우 링크를 즉시 반환합니다. + if (req.eventStatus.isInviteUrlEnabled) return res.json({ inviteUrl }); + + // 초대 링크를 활성화합니다. + const { modifiedCount } = await eventStatusModel.updateOne( + { + _id: req.eventStatus._id, + isInviteUrlEnabled: false, + }, + { + isInviteUrlEnabled: true, + } + ); + if (modifiedCount !== 1) + return res + .status(500) + .json({ error: "Invites/create : internal server error" }); + + return res.json({ inviteUrl }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Invites/create : internal server error" }); + } +}; + +module.exports = { + searchInviterHandler, + createInviteUrlHandler, +}; diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index 9189bae4..07730574 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -3,9 +3,159 @@ const { itemModel, transactionModel, } = require("../modules/stores/mongo"); +const { userModel } = require("../../modules/stores/mongo"); +const { isLogin, getLoginInfo } = require("../../modules/auths/login"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); +const contracts = require("../modules/contracts"); + +const getItemsHandler = async (req, res) => { + try { + const items = await itemModel + .find( + {}, + "_id name description imageUrl price isDisabled itemType realStock" + ) + .lean(); + res.json({ items }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/ : internal server error" }); + } +}; + +const getItemHandler = async (req, res) => { + try { + const { itemId } = req.params; + const item = await itemModel + .findById( + itemId, + "_id name description imageUrl price isDisabled itemType realStock" + ) + .lean(); + if (!item) return res.status(400).json({ error: "Items/ : invalid item" }); + + res.json({ item }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/ : internal server error" }); + } +}; + +// 유도 과정은 services/publicNotice.js 파일에 정의된 calculateProbabilityV2 함수의 주석 참조 +const calculateWinProbability = (realStock, users, amount, totalAmount) => { + if (users.length <= realStock) return 1; + + const base = Math.pow( + 1 - realStock / users.length, + users.length / totalAmount + ); + return 1 - Math.pow(base, amount); +}; + +const getItemLeaderboardHandler = async (req, res) => { + try { + // 상품 정보를 가져옵니다. + const { itemId } = req.params; + const item = await itemModel.findOne({ _id: itemId, itemType: 0 }).lean(); + if (!item) + return res + .status(400) + .json({ error: "Items/leaderboard : invalid item" }); + + // 해당 상품을 구매한 유저들의 목록을 가져옵니다. + const users = await transactionModel.aggregate([ + { + $match: { + type: "use", + itemId: item._id, + }, + }, + { + $group: { + _id: "$userId", + amount: { $sum: "$itemAmount" }, + }, + }, + { + $lookup: { + from: eventStatusModel.collection.name, + localField: "_id", + foreignField: "userId", + as: "eventStatus", + }, + }, + { + $match: { + "eventStatus.0.isBanned": false, + }, + }, + { + $sort: { amount: -1 }, + }, + ]); + + // 리더보드 생성을 위해 필요한 정보를 계산합니다. + const totalAmount = users.reduce((acc, user) => acc + user.amount, 0); + const rankMap = new Map( + users + .map((user) => user.amount) + .reduce((acc, amount, index) => { + if (acc.length === 0 || acc[acc.length - 1][0] !== amount) { + acc.push([amount, index + 1]); + } + return acc; + }, []) + ); + + // 리더보드를 생성합니다. + const leaderboardBase = users.map((user) => ({ + userId: user._id, + amount: user.amount, + probability: calculateWinProbability( + item.realStock, + users, + user.amount, + totalAmount + ), + rank: rankMap.get(user.amount), + })); + const leaderboard = await Promise.all( + leaderboardBase + .filter((user) => user.rank <= 20) + .map(async (user) => { + const userInfo = await userModel.findById(user.userId).lean(); + return { + nickname: userInfo.nickname, + profileImageUrl: userInfo.profileImageUrl, + amount: user.amount, + probability: user.probability, + rank: user.rank, + }; + }) + ); + + const userId = isLogin(req) ? getLoginInfo(req).oid : null; + const user = leaderboardBase.find( + (user) => user.userId.toString() === userId + ); + + return res.json({ + leaderboard, + totalAmount, + totalUser: users.length, + amount: user?.amount, + probability: user?.probability, + rank: user?.rank, + }); + } catch (err) { + logger.error(err); + res + .status(500) + .json({ error: "Items/leaderboard : internal server error" }); + } +}; const updateEventStatus = async ( userId, @@ -22,199 +172,217 @@ const updateEventStatus = async ( } ); -const hideItemStock = (item) => { - item.stock = item.stock > 0 ? 1 : 0; - return item; -}; +// 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. +// +// const getRandomItem = async (req, depth) => { +// if (depth >= 10) { +// logger.error(`User ${req.userOid} failed to open random box`); +// return null; +// } -const getRandomItem = async (req, depth) => { - if (depth >= 10) { - logger.error(`User ${req.userOid} failed to open random box`); - return null; - } - - const items = await itemModel - .find({ - isRandomItem: true, - stock: { $gt: 0 }, - isDisabled: false, - }) - .lean(); - const randomItems = items - .map((item) => Array(item.randomWeight).fill(item)) - .reduce((a, b) => a.concat(b), []); - const dumpRandomItems = randomItems - .map((item) => item._id.toString()) - .join(","); - - logger.info( - `User ${req.userOid}'s ${ - depth + 1 - }th random box probability is: [${dumpRandomItems}]` - ); +// const items = await itemModel +// .find({ +// isRandomItem: true, +// stock: { $gt: 0 }, +// isDisabled: false, +// }) +// .lean(); +// const randomItems = items +// .map((item) => Array(item.randomWeight).fill(item)) +// .reduce((a, b) => a.concat(b), []); +// const dumpRandomItems = randomItems +// .map((item) => item._id.toString()) +// .join(","); - if (randomItems.length === 0) return null; +// logger.info( +// `User ${req.userOid}'s ${ +// depth + 1 +// }th random box probability is: [${dumpRandomItems}]` +// ); - const randomItem = - randomItems[Math.floor(Math.random() * randomItems.length)]; - try { - // 1단계: 재고를 차감합니다. - const newRandomItem = await itemModel - .findOneAndUpdate( - { _id: randomItem._id, stock: { $gt: 0 } }, - { - $inc: { - stock: -1, - }, - }, - { - new: true, - fields: { - itemType: 0, - isRandomItem: 0, - randomWeight: 0, - }, - } - ) - .lean(); - if (!newRandomItem) { - throw new Error(`Item ${randomItem._id.toString()} was already sold out`); - } +// if (randomItems.length === 0) return null; - // 2단계: 유저 정보를 업데이트합니다. - await updateEventStatus(req.userOid, { - ticket1Delta: randomItem.itemType === 1 ? 1 : 0, - ticket2Delta: randomItem.itemType === 2 ? 1 : 0, - }); +// const randomItem = +// randomItems[Math.floor(Math.random() * randomItems.length)]; +// try { +// // 1단계: 재고를 차감합니다. +// const newRandomItem = await itemModel +// .findOneAndUpdate( +// { _id: randomItem._id, stock: { $gt: 0 } }, +// { +// $inc: { +// stock: -1, +// }, +// }, +// { +// new: true, +// fields: { +// itemType: 0, +// isRandomItem: 0, +// randomWeight: 0, +// }, +// } +// ) +// .lean(); +// if (!newRandomItem) { +// throw new Error(`Item ${randomItem._id.toString()} was already sold out`); +// } - // 3단계: Transaction을 추가합니다. - const transaction = new transactionModel({ - type: "use", - amount: 0, - userId: req.userOid, - item: randomItem._id, - itemType: randomItem.itemType, - comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, - }); - await transaction.save(); +// // 2단계: 유저 정보를 업데이트합니다. +// await updateEventStatus(req.userOid, { +// ticket1Delta: randomItem.itemType === 1 ? 1 : 0, +// ticket2Delta: randomItem.itemType === 2 ? 1 : 0, +// }); - return newRandomItem; - } catch (err) { - logger.error(err); - logger.warn( - `User ${req.userOid}'s ${depth + 1}th random box failed due to exception` - ); +// // 3단계: Transaction을 추가합니다. +// const transaction = new transactionModel({ +// type: "use", +// amount: 0, +// userId: req.userOid, +// itemId: randomItem._id, +// comment: `랜덤박스에서 "${randomItem.name}" 1개를 획득했습니다.`, +// }); +// await transaction.save(); - return await getRandomItem(req, depth + 1); - } -}; +// return newRandomItem; +// } catch (err) { +// logger.error(err); +// logger.warn( +// `User ${req.userOid}'s ${depth + 1}th random box failed due to exception` +// ); -const listHandler = async (_, res) => { - try { - const items = await itemModel - .find( - {}, - "name imageUrl instagramStoryStickerImageUrl price description isDisabled stock itemType" - ) - .lean(); - res.json({ items: items.map(hideItemStock) }); - } catch (err) { - logger.error(err); - res.status(500).json({ error: "Items/List : internal server error" }); - } -}; +// return await getRandomItem(req, depth + 1); +// } +// }; -const purchaseHandler = async (req, res) => { +const purchaseItemHandler = async (req, res) => { try { const { itemId } = req.params; - const item = await itemModel.findOne({ _id: itemId }).lean(); + const item = await itemModel.findById(itemId).lean(); if (!item) - return res.status(400).json({ error: "Items/Purchase : invalid Item" }); + return res.status(400).json({ error: "Items/purchase : invalid Item" }); - // 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다. + const { amount } = req.body; + const totalPrice = item.price * amount; + + // 구매 가능 조건: 재화가 충분하며, 재고가 남아있으며, 판매 중인 상품이어야 합니다. if (item.isDisabled) - return res.status(400).json({ error: "Items/Purchase : disabled item" }); - if (req.eventStatus.creditAmount < item.price) + return res.status(400).json({ error: "Items/purchase : disabled item" }); + if (req.eventStatus.creditAmount < totalPrice) return res .status(400) - .json({ error: "Items/Purchase : not enough credit" }); - if (item.stock <= 0) + .json({ error: "Items/purchase : not enough credit" }); + if (item.stock < amount) return res .status(400) - .json({ error: "Items/Purchase : item out of stock" }); + .json({ error: "Items/purchase : item out of stock" }); // 1단계: 재고를 차감합니다. const { modifiedCount } = await itemModel.updateOne( - { _id: item._id, stock: { $gt: 0 } }, - { - $inc: { - stock: -1, - }, - } + { _id: item._id, stock: { $gte: amount } }, + { $inc: { stock: -amount } } ); if (modifiedCount === 0) return res .status(400) - .json({ error: "Items/Purchase : item out of stock" }); + .json({ error: "Items/purchase : item out of stock" }); - // 2단계: 유저 정보를 업데이트합니다. - await updateEventStatus(req.userOid, { - creditDelta: -item.price, - ticket1Delta: item.itemType === 1 ? 1 : 0, - ticket2Delta: item.itemType === 2 ? 1 : 0, - }); + if (item.itemType !== 3) { + // 랜덤박스가 아닌 상품을 구입한 경우 + // 2단계: 유저 정보를 업데이트합니다. + await updateEventStatus(req.userOid, { + creditDelta: -totalPrice, + ticket1Delta: item.itemType === 1 ? amount : 0, + ticket2Delta: item.itemType === 2 ? amount : 0, + }); - // 3단계: Transaction을 추가합니다. - const transaction = new transactionModel({ - type: "use", - amount: item.price, - userId: req.userOid, - item: item._id, - itemType: item.itemType, - comment: `${eventConfig?.credit.name} ${item.price}개를 사용해 "${item.name}" 1개를 획득했습니다.`, - }); - await transaction.save(); + // 3단계: 출금 내역을 추가합니다. + const transaction = new transactionModel({ + type: "use", + amount: totalPrice, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 사용해 "${item.name}" ${amount}개를 획득했습니다.`, + }); + await transaction.save(); - // 4단계: 랜덤박스인 경우 아이템을 추첨합니다. - if (item.itemType !== 3) return res.json({ result: true }); + // 4단계: 퀘스트를 완료 처리합니다. + await contracts.completeItemPurchaseQuest( + req.userOid, + transaction.createdAt + ); - const randomItem = await getRandomItem(req, 0); - if (!randomItem) { - // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다. - // TODO: Transactions 도입 후 이 코드는 삭제합니다. - logger.info(`User ${req.userOid}'s status will be restored`); + return res.json({ result: true }); + } else { + // 랜덤박스를 구입한 경우 + // 2단계: 대박(40%)인지 쪽박(60%)인지 결정합니다. + const isJackpot = Math.random() < 0.4; + const creditDelta = isJackpot ? totalPrice : -totalPrice; - await transactionModel.deleteOne({ _id: transaction._id }); - await updateEventStatus(req.userOid, { - creditDelta: item.price, - }); - await itemModel.updateOne( - { _id: item._id }, - { - $inc: { - stock: 1, - }, - } - ); + // 3단계: 유저 정보를 업데이트합니다. + await updateEventStatus(req.userOid, { creditDelta }); - logger.info(`User ${req.userOid}'s status was successfully restored`); + // 4단계: 입출금 내역을 추가합니다. + if (isJackpot) { + const transaction = new transactionModel({ + type: "get", + amount: creditDelta, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용해 대박을 터뜨렸습니다.`, + }); + await transaction.save(); + } else { + const transaction = new transactionModel({ + type: "use", + amount: -creditDelta, + userId: req.userOid, + itemId: item._id, + itemAmount: amount, + comment: `${eventConfig?.credit.name} ${totalPrice}개를 "${item.name}"에 사용했지만 쪽박을 맞았습니다.`, + }); + await transaction.save(); + } - return res - .status(500) - .json({ error: "Items/Purchase : random box error" }); + return res.json({ result: true, isJackpot }); } - res.json({ - result: true, - reward: hideItemStock(randomItem), - }); + // const randomItem = await getRandomItem(req, 0); + // if (!randomItem) { + // // 랜덤박스가 실패한 경우, 상태를 구매 이전으로 되돌립니다. + // // TODO: Transactions 도입 후 이 코드는 삭제합니다. + // logger.info(`User ${req.userOid}'s status will be restored`); + + // await transactionModel.deleteOne({ _id: transaction._id }); + // await updateEventStatus(req.userOid, { + // creditDelta: item.price, + // }); + // await itemModel.updateOne( + // { _id: item._id }, + // { + // $inc: { + // stock: 1, + // }, + // } + // ); + + // logger.info(`User ${req.userOid}'s status was successfully restored`); + + // return res + // .status(500) + // .json({ error: "Items/purchase : random box error" }); + // } } catch (err) { logger.error(err); - res.status(500).json({ error: "Items/Purchase : internal server error" }); + res.status(500).json({ error: "Items/purchase : internal server error" }); } }; module.exports = { - listHandler, - purchaseHandler, + getItemsHandler, + getItemHandler, + getItemLeaderboardHandler, + purchaseItemHandler, }; diff --git a/src/lottery/services/quests.js b/src/lottery/services/quests.js index ae1472bc..5a0c6ae6 100644 --- a/src/lottery/services/quests.js +++ b/src/lottery/services/quests.js @@ -3,20 +3,38 @@ const logger = require("../../modules/logger"); const contracts = require("../modules/contracts"); -const completeHandler = async (req, res) => { +const completeQuestHandler = async (req, res) => { try { const quest = contracts.quests[req.params.questId]; if (!quest || !quest.isApiRequired) - return res.status(400).json({ error: "Quests/Complete: invalid Quest" }); + return res.status(400).json({ error: "Quests/complete: invalid quest" }); + + // 출석 체크 퀘스트는 하루에 1번만 완료하도록 제한합니다. + if (quest.id === "dailyAttendance") { + const todayMidnight = new Date(req.timestamp); + todayMidnight.setHours(0, 0, 0, 0); + + const tomorrowMidnight = new Date(todayMidnight); + tomorrowMidnight.setDate(tomorrowMidnight.getDate() + 1); + + // 오늘 완료된 dailyAttendance 퀘스트가 있는지 확인합니다. + const completedQuest = req.eventStatus.completedQuests.find( + ({ questId, completedAt }) => + questId === quest.id && + completedAt >= todayMidnight && + completedAt < tomorrowMidnight + ); + if (completedQuest) return res.json({ result: false }); + } const result = await completeQuest(req.userOid, req.timestamp, quest); res.json({ result: !!result }); // boolean으로 변환하기 위해 !!를 사용합니다. } catch (err) { logger.error(err); - res.status(500).json({ error: "Quests/Complete: internal server error" }); + res.status(500).json({ error: "Quests/complete: internal server error" }); } }; module.exports = { - completeHandler, + completeQuestHandler, }; diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js index 1d920870..fe976e28 100644 --- a/src/lottery/services/transactions.js +++ b/src/lottery/services/transactions.js @@ -1,25 +1,34 @@ const { transactionModel } = require("../modules/stores/mongo"); +const { + transactionPopulateOption, +} = require("../modules/populates/transactions"); const logger = require("../../modules/logger"); -const hideItemStock = (transaction) => { - if (transaction.item) { - transaction.item.stock = transaction.item.stock > 0 ? 1 : 0; +const formatTransaction = (transaction) => { + if (transaction.itemId) { + transaction.item = transaction.itemId; + delete transaction.itemId; } return transaction; }; const getUserTransactionsHandler = async (req, res) => { try { - // userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다. const transactions = await transactionModel - .find({ userId: req.userOid }, "_id type amount questId comment createAt") + .find( + { userId: req.userOid }, + "type amount questId itemId comment createdAt" + ) + .populate(transactionPopulateOption) .lean(); - if (transactions) - res.json({ - transactions, - }); - else - res.status(500).json({ error: "Transactions/ : internal server error" }); + if (!transactions) + return res + .status(500) + .json({ error: "Transactions/ : internal server error" }); + + res.json({ + transactions: transactions.map(formatTransaction), + }); } catch (err) { logger.error(err); res.status(500).json({ error: "Transactions/ : internal server error" }); diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js deleted file mode 100644 index 23272af2..00000000 --- a/src/middlewares/auth.js +++ /dev/null @@ -1,17 +0,0 @@ -// 로그인된 상태에만 접근할 수 있는 라우터(rooms)를 위한 미들웨어입니다. - -const { isLogin, getLoginInfo } = require("../modules/auths/login"); - -const authMiddleware = (req, res, next) => { - if (!isLogin(req)) { - res.status(403).json({ - error: "not logged in", - }); - } else { - const { oid } = getLoginInfo(req); - req.userOid = oid; - next(); - } -}; - -module.exports = authMiddleware; diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts new file mode 100644 index 00000000..c9f6c0d7 --- /dev/null +++ b/src/middlewares/auth.ts @@ -0,0 +1,17 @@ +// 로그인된 상태에만 접근할 수 있는 라우터(rooms)를 위한 미들웨어입니다. +import type { RequestHandler } from "express"; +import { isLogin, getLoginInfo } from "@/modules/auths/login"; + +const authMiddleware: RequestHandler = (req, res, next) => { + if (!isLogin(req)) + return res.status(403).json({ + error: "not logged in", + }); + + const { oid } = getLoginInfo(req); + req.userOid = oid; + + next(); +}; + +export default authMiddleware; diff --git a/src/middlewares/authAdmin.js b/src/middlewares/authAdmin.js deleted file mode 100644 index 7b69a795..00000000 --- a/src/middlewares/authAdmin.js +++ /dev/null @@ -1,31 +0,0 @@ -// 관리자 유무를 확인하기 위한 미들웨어입니다. - -const { isLogin, getLoginInfo } = require("../modules/auths/login"); -const { userModel, adminIPWhitelistModel } = require("../modules/stores/mongo"); - -const authAdminMiddleware = async (req, res, next) => { - try { - // 로그인 여부를 확인 - if (!isLogin(req)) return res.redirect(req.origin); - - // 관리자 유무를 확인 - const { oid } = getLoginInfo(req); - const user = await userModel.findOne({ _id: oid, withdraw: false }); - if (!user.isAdmin) return res.redirect(req.origin); - - // 접속한 IP가 화이트리스트에 있는지 확인 - const ipWhitelist = await adminIPWhitelistModel.find({}); - if (!req.clientIP) return res.redirect(req.origin); - if ( - ipWhitelist.length > 0 && - ipWhitelist.map((x) => x.ip).indexOf(req.clientIP) < 0 - ) - return res.redirect(req.origin); - - next(); - } catch (e) { - res.redirect(req.origin); - } -}; - -module.exports = authAdminMiddleware; diff --git a/src/middlewares/authAdmin.ts b/src/middlewares/authAdmin.ts new file mode 100644 index 00000000..5f323cc2 --- /dev/null +++ b/src/middlewares/authAdmin.ts @@ -0,0 +1,33 @@ +// 관리자 유무를 확인하기 위한 미들웨어입니다. +import type { RequestHandler } from "express"; +import { isLogin, getLoginInfo } from "@/modules/auths/login"; +import { userModel, adminIPWhitelistModel } from "@/modules/stores/mongo"; + +const authAdminMiddleware: RequestHandler = async (req, res, next) => { + const redirectUrl = req.origin ?? "/"; + + try { + // 로그인 여부를 확인 + if (!isLogin(req)) return res.redirect(redirectUrl); + + // 관리자 유무를 확인 + const { oid } = getLoginInfo(req); + const user = await userModel.findOne({ _id: oid, withdraw: false }); + if (!user?.isAdmin) return res.redirect(redirectUrl); + + // 접속한 IP가 화이트리스트에 있는지 확인 + const ipWhitelist = await adminIPWhitelistModel.find({}); + if (!req.clientIP) return res.redirect(redirectUrl); + if ( + ipWhitelist.length > 0 && + ipWhitelist.map((x) => x.ip).indexOf(req.clientIP) < 0 + ) + return res.redirect(redirectUrl); + + next(); + } catch (e) { + return res.redirect(redirectUrl); + } +}; + +export default authAdminMiddleware; diff --git a/src/middlewares/ban.ts b/src/middlewares/ban.ts new file mode 100644 index 00000000..d9a1f9d6 --- /dev/null +++ b/src/middlewares/ban.ts @@ -0,0 +1,20 @@ +import type { RequestHandler } from "express"; +import { validateServiceBanRecord } from "@/modules/ban"; + +const serviceMapper = new Map([ + ["/rooms/create", "service"], + ["/rooms/join", "service"], +]); + +const banMiddleware: RequestHandler = async (req, res, next) => { + const banErrorMessage = await validateServiceBanRecord( + req, + serviceMapper.get(req.originalUrl) || "" + ); + if (banErrorMessage !== undefined) { + return res.status(400).json({ error: banErrorMessage }); + } + next(); +}; + +export default banMiddleware; diff --git a/src/middlewares/cors.js b/src/middlewares/cors.js deleted file mode 100644 index 0b644243..00000000 --- a/src/middlewares/cors.js +++ /dev/null @@ -1,8 +0,0 @@ -var cors = require("cors"); -const { corsWhiteList } = require("../../loadenv"); - -module.exports = cors({ - origin: corsWhiteList, - credentials: true, - exposedHeaders: ["Date"], -}); diff --git a/src/middlewares/cors.ts b/src/middlewares/cors.ts new file mode 100644 index 00000000..63559565 --- /dev/null +++ b/src/middlewares/cors.ts @@ -0,0 +1,10 @@ +import cors from "cors"; +import { corsWhiteList } from "@/loadenv"; + +const corsMiddleware = cors({ + origin: corsWhiteList, + credentials: true, + exposedHeaders: ["Date"], +}); + +export default corsMiddleware; diff --git a/src/middlewares/errorHandler.js b/src/middlewares/errorHandler.ts similarity index 65% rename from src/middlewares/errorHandler.js rename to src/middlewares/errorHandler.ts index 369391c6..f130caa8 100644 --- a/src/middlewares/errorHandler.js +++ b/src/middlewares/errorHandler.ts @@ -1,24 +1,27 @@ -const logger = require("../modules/logger"); +import type { ErrorRequestHandler } from "express"; +import logger from "@/modules/logger"; /** * Express app에서 사용할 custom global error handler를 정의합니다. * @summary Express 핸들러에서 발생한 uncaught exception은 이 핸들러를 통해 처리됩니다. * Express에서 제공하는 기본 global error handler는 클라이언트에 오류 발생 call stack을 그대로 반환합니다. * 이 때문에 클라이언트에게 잠재적으로 보안 취약점을 노출할 수 있으므로, call stack을 반환하지 않는 error handler를 정의합니다. - * @param {Error} err - 오류 객체 - * @param {Express.Request} req - 요청 객체 - * @param {Express.Response} res - 응답 객체 - * @param {Function} next - 다음 미들웨어 함수. Express에서는 next 함수에 err 인자를 넘겨주면 기본 global error handler가 호출됩니다. + * @param err - 오류 객체 + * @param req - 요청 객체 + * @param res - 응답 객체 + * @param next - 다음 미들웨어 함수. Express에서는 next 함수에 err 인자를 넘겨주면 기본 global error handler가 호출됩니다. */ -const errorHandler = (err, req, res, next) => { +const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + logger.error(err); + // 이미 클라이언트에 HTTP 응답 헤더를 전송한 경우, 응답 헤더를 다시 전송하지 않아야 합니다. // 클라이언트에게 스트리밍 형태로 응답을 전송하는 도중 오류가 발생하는 경우가 여기에 해당합니다. // 이럴 때 기본 global error handler를 호출하면 기본 global error handler가 클라이언트와의 연결을 종료시켜 줍니다. - logger.error(err); if (res.headersSent) { - return next(err); + next(err); + } else { + return res.status(500).send("internal server error"); } - res.status(500).send("internal server error"); }; -module.exports = errorHandler; +export default errorHandler; diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts new file mode 100644 index 00000000..7484df61 --- /dev/null +++ b/src/middlewares/index.ts @@ -0,0 +1,13 @@ +// middleware를 모아 export합니다. +export { default as authMiddleware } from "./auth"; +export { default as authAdminMiddleware } from "./authAdmin"; +export { default as banMiddleware } from "./ban"; +export { default as corsMiddleware } from "./cors"; +export { default as errorHandler } from "./errorHandler"; +export { default as informationMiddleware } from "./information"; +export { default as limitRateMiddleware } from "./limitRate"; +export { default as originValidatorMiddleware } from "./originValidator"; +export { default as responseTimeMiddleware } from "./responseTime"; +export { default as sessionMiddleware } from "./session"; +export { default as validatorMiddleware } from "./validator"; +export * from "./zod"; diff --git a/src/middlewares/information.js b/src/middlewares/information.js deleted file mode 100644 index f7197d1f..00000000 --- a/src/middlewares/information.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = (req, res, next) => { - req.clientIP = req.headers["x-forwarded-for"] || req.connection.remoteAddress; - req.timestamp = Date.now(); - next(); -}; diff --git a/src/middlewares/information.ts b/src/middlewares/information.ts new file mode 100644 index 00000000..91b3d66f --- /dev/null +++ b/src/middlewares/information.ts @@ -0,0 +1,11 @@ +import type { RequestHandler } from "express"; + +const informationMiddleware: RequestHandler = (req, res, next) => { + req.clientIP = + (req.headers["x-forwarded-for"] as string | undefined) || + req.connection.remoteAddress; + req.timestamp = Date.now(); + next(); +}; + +export default informationMiddleware; diff --git a/src/middlewares/limitRate.js b/src/middlewares/limitRate.ts similarity index 54% rename from src/middlewares/limitRate.js rename to src/middlewares/limitRate.ts index c5069c8f..18a3203b 100644 --- a/src/middlewares/limitRate.js +++ b/src/middlewares/limitRate.ts @@ -1,14 +1,10 @@ -const rateLimit = require("express-rate-limit"); +import rateLimit from "express-rate-limit"; -const limiter = rateLimit({ +const limitRateMiddleware = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 1500, // Limit each IP to 1500 requests per `window` (here, per 15 minutes) standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers - validate: { - default: true, - trustProxy: false, // Disable the validation error caused by 'trust proxy' set to true - }, }); -module.exports = limiter; +export default limitRateMiddleware; diff --git a/src/middlewares/originValidator.js b/src/middlewares/originValidator.ts similarity index 61% rename from src/middlewares/originValidator.js rename to src/middlewares/originValidator.ts index 2aec851d..330226cc 100644 --- a/src/middlewares/originValidator.js +++ b/src/middlewares/originValidator.ts @@ -1,13 +1,17 @@ -module.exports = (req, res, next) => { +import type { RequestHandler } from "express"; + +const originValidatorMiddleware: RequestHandler = (req, res, next) => { req.origin = req.headers.origin || req.headers.referer || req.session?.loginAfterState?.redirectOrigin; // sparcssso/callback 요청은 헤더에 origin이 없음 - if (!req.origin) { + if (!req.origin) return res.status(400).json({ error: "Bad Request : request must have origin in header", }); - } + next(); }; + +export default originValidatorMiddleware; diff --git a/src/middlewares/responseTime.js b/src/middlewares/responseTime.js deleted file mode 100644 index 1d674364..00000000 --- a/src/middlewares/responseTime.js +++ /dev/null @@ -1,11 +0,0 @@ -const logger = require("../modules/logger"); -const responseTime = require("response-time"); - -module.exports = responseTime((req, res, time) => { - const { method, originalUrl, clientIP } = req; - const { statusCode } = res; - const userId = req.session?.loginInfo?.id || "anonymous"; - logger.info( - `${userId}(${clientIP}) "${method} ${originalUrl}" ${statusCode} on ${time}ms` - ); -}); diff --git a/src/middlewares/responseTime.ts b/src/middlewares/responseTime.ts new file mode 100644 index 00000000..ba84115a --- /dev/null +++ b/src/middlewares/responseTime.ts @@ -0,0 +1,16 @@ +import type { Request, Response } from "express"; +import responseTime from "response-time"; +import logger from "@/modules/logger"; + +const responseTimeMiddleware = responseTime( + (req: Request, res: Response, time: number) => { + const { method, originalUrl, clientIP } = req; + const { statusCode } = res; + const userId = req.session?.loginInfo?.oid || "anonymous"; + logger.info( + `${userId}(${clientIP}) "${method} ${originalUrl}" ${statusCode} on ${time}ms` + ); + } +); + +export default responseTimeMiddleware; diff --git a/src/middlewares/session.js b/src/middlewares/session.js deleted file mode 100644 index 5412ba1c..00000000 --- a/src/middlewares/session.js +++ /dev/null @@ -1,14 +0,0 @@ -const expressSession = require("express-session"); -const { nodeEnv, session: sessionConfig } = require("../../loadenv"); -const sessionStore = require("../modules/stores/sessionStore"); - -module.exports = expressSession({ - secret: sessionConfig.secret, - resave: false, - saveUninitialized: false, - store: sessionStore, - cookie: { - maxAge: sessionConfig.expiry, - secure: nodeEnv === "production", - }, -}); diff --git a/src/middlewares/session.ts b/src/middlewares/session.ts new file mode 100644 index 00000000..3486154e --- /dev/null +++ b/src/middlewares/session.ts @@ -0,0 +1,40 @@ +import expressSession from "express-session"; +import { nodeEnv, session as sessionConfig } from "@/loadenv"; +import type { LoginInfo } from "@/modules/auths/login"; +import sessionStore from "@/modules/stores/sessionStore"; + +// 세션에 저장할 데이터 타입을 지정합니다. +declare module "express-session" { + interface SessionData { + /** 사용자 로그인 정보 */ + loginInfo?: LoginInfo; + /** 현재 로그인된 사용자가 앱으로 접속했는지 여부 */ + isApp?: boolean; + /** SPARCS SSO 로그인 시 state와 로그인 후 redirect 주소를 저장할 object. 타입 수정 필요. */ + loginAfterState?: { + state?: string; + redirectOrigin?: string; + redirectPath?: string; + }; + /** 앱 로그인용 access token */ + accessToken?: string; + /** 앱 로그인용 refresh token */ + refreshToken?: string; + /** FCM용 device token */ + deviceToken?: string; + } +} + +const sessionMiddleware = expressSession({ + secret: sessionConfig.secret, + resave: false, + saveUninitialized: false, + store: sessionStore, + cookie: { + maxAge: sessionConfig.expiry, + // nodeEnv가 production일 때만 secure cookie를 사용합니다. + secure: nodeEnv === "production", + }, +}); + +export default sessionMiddleware; diff --git a/src/middlewares/validator.js b/src/middlewares/validator.js deleted file mode 100644 index 8bdeb4c4..00000000 --- a/src/middlewares/validator.js +++ /dev/null @@ -1,11 +0,0 @@ -const { validationResult } = require("express-validator"); - -module.exports = (req, res, next) => { - const validationErrors = validationResult(req); - if (!validationErrors.isEmpty()) { - return res.status(400).json({ - error: "validation : bad request", - }); - } - next(); -}; diff --git a/src/middlewares/validator.ts b/src/middlewares/validator.ts new file mode 100644 index 00000000..aafb165a --- /dev/null +++ b/src/middlewares/validator.ts @@ -0,0 +1,14 @@ +import type { RequestHandler } from "express"; +import { validationResult } from "express-validator"; + +const validatorMiddleware: RequestHandler = (req, res, next) => { + const validationErrors = validationResult(req); + if (!validationErrors.isEmpty()) + return res.status(400).json({ + error: "validation : bad request", + }); + + next(); +}; + +export default validatorMiddleware; diff --git a/src/middlewares/zod.js b/src/middlewares/zod.js index 63f5668a..b8660634 100644 --- a/src/middlewares/zod.js +++ b/src/middlewares/zod.js @@ -1,4 +1,4 @@ -const logger = require("../modules/logger"); +const logger = require("@/modules/logger").default; const parseZodErrors = (statusCode, errors, res) => { const error_message = errors; diff --git a/src/modules/adminResource.js b/src/modules/adminResource.js index f5ba3eb8..19deb694 100644 --- a/src/modules/adminResource.js +++ b/src/modules/adminResource.js @@ -1,5 +1,5 @@ const { buildFeature } = require("adminjs"); -const { adminLogModel } = require("./stores/mongo"); +const { adminLogModel } = require("@/modules/stores/mongo"); const createLog = async (req, action, target) => { const newLog = new adminLogModel({ diff --git a/src/modules/auths/jwt.js b/src/modules/auths/jwt.js deleted file mode 100644 index 52945347..00000000 --- a/src/modules/auths/jwt.js +++ /dev/null @@ -1,43 +0,0 @@ -const jwt = require("jsonwebtoken"); -const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = - require("../../../loadenv").jwt; - -const signJwt = async ({ id, type }) => { - const payload = { - id: id, - type: type, - }; - - const options = { ...option }; - - if (type === "refresh") { - options.expiresIn = "30d"; - } - if (type === "access") { - options.expiresIn = "14d"; - } - - const result = { - token: jwt.sign(payload, secretKey, options), - }; - return result; -}; - -const verifyJwt = async (token) => { - let decoded; - try { - decoded = jwt.verify(token, secretKey); - } catch (err) { - if (err.message === "jwt expired") { - return TOKEN_EXPIRED; - } else { - return TOKEN_INVALID; - } - } - return decoded; -}; - -module.exports = { - sign: signJwt, - verify: verifyJwt, -}; diff --git a/src/modules/auths/jwt.ts b/src/modules/auths/jwt.ts new file mode 100644 index 00000000..ee009330 --- /dev/null +++ b/src/modules/auths/jwt.ts @@ -0,0 +1,47 @@ +import jwt, { type SignOptions } from "jsonwebtoken"; +import { jwt as jwtConfig } from "@/loadenv"; + +const { secretKey, option, TOKEN_EXPIRED, TOKEN_INVALID } = jwtConfig; + +type TokenType = "access" | "refresh"; + +interface SignType { + id: string; + type: TokenType; +} + +export const sign = async ({ id, type }: SignType) => { + const payload = { + id, + type, + }; + + const options: SignOptions = { ...option }; + + if (type === "refresh") { + options.expiresIn = "30d"; + } + if (type === "access") { + options.expiresIn = "14d"; + } + + const result = { + token: jwt.sign(payload, secretKey, options), + }; + return result; +}; + +export const verify = async (token: string) => { + let decoded; + try { + decoded = jwt.verify(token, secretKey); + } catch (err) { + if (err instanceof Error) { + if (err.message === "jwt expired") { + return TOKEN_EXPIRED; + } + } + return TOKEN_INVALID; + } + return decoded; +}; diff --git a/src/modules/auths/login.js b/src/modules/auths/login.ts similarity index 65% rename from src/modules/auths/login.js rename to src/modules/auths/login.ts index 9c72434f..962c260f 100644 --- a/src/modules/auths/login.js +++ b/src/modules/auths/login.ts @@ -1,7 +1,16 @@ -const { session: sessionConfig } = require("../../../loadenv"); -const logger = require("../logger"); +import type { Request } from "express"; +import { session as sessionConfig } from "@/loadenv"; +import logger from "@/modules/logger"; -const getLoginInfo = (req) => { +export interface LoginInfo { + id: string; + sid: string; + oid: string; + name: string; + time: number; +} + +export const getLoginInfo = (req: Request) => { if (req.session.loginInfo) { const { id, sid, oid, name, time } = req.session.loginInfo; const timeFlow = Date.now() - time; @@ -15,17 +24,23 @@ const getLoginInfo = (req) => { return { id: undefined, sid: undefined, oid: undefined, name: undefined }; }; -const isLogin = (req) => { +export const isLogin = (req: Request) => { const loginInfo = getLoginInfo(req); if (loginInfo.id) return true; - else return false; + return false; }; -const login = (req, sid, id, oid, name) => { +export const login = ( + req: Request, + sid: string, + id: string, + oid: string, + name: string +) => { req.session.loginInfo = { sid, id, oid, name, time: Date.now() }; }; -const logout = (req) => { +export const logout = (req: Request) => { // 로그아웃 전 socket.io 소켓들 연결부터 끊기 const io = req.app.get("io"); if (io) io.in(`session-${req.session.id}`).disconnectSockets(true); @@ -34,10 +49,3 @@ const logout = (req) => { if (err) logger.error(err); }); }; - -module.exports = { - getLoginInfo, - isLogin, - login, - logout, -}; diff --git a/src/modules/ban.ts b/src/modules/ban.ts new file mode 100644 index 00000000..bb62214a --- /dev/null +++ b/src/modules/ban.ts @@ -0,0 +1,41 @@ +import type { Request } from "express"; +import logger from "@/modules/logger"; +import { banModel } from "@/modules/stores/mongo"; + +export const validateServiceBanRecord = async ( + req: Request, + service: string +) => { + let banRecord = undefined; + + try { + // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 + const bans = await banModel + .find({ + userSid: req.session.loginInfo!.sid, + expireAt: { + $gte: req.timestamp, + }, + serviceName: service, + }) + .sort({ expireAt: -1 }); + if (bans.length > 0) { + // 가장 expireAt이 큰 정지 기록만 반환함. + banRecord = bans[0]; + } + } catch (err) { + logger.error(err); + return; + } + if (banRecord !== undefined) { + const formattedExpireAt = banRecord.expireAt + .toISOString() + .replace("T", " ") + .split(".")[0]; + const banErrorMessage = `${req.originalUrl} : user ${req.userOid} (${ + req.session.loginInfo!.sid + }) is temporarily restricted from service until ${formattedExpireAt}.`; + return banErrorMessage; + } + return; +}; diff --git a/src/modules/email.js b/src/modules/email.js index 2ccfce8c..6ab6eb72 100644 --- a/src/modules/email.js +++ b/src/modules/email.js @@ -1,6 +1,6 @@ const nodemailer = require("nodemailer"); -const logger = require("./logger"); -const { nodeEnv } = require("../../loadenv"); +const logger = require("@/modules/logger").default; +const { nodeEnv } = require("@/loadenv"); /** * production 환경에서 메일을 전송하기 위해 사용되는 agent입니다. diff --git a/src/modules/fare.js b/src/modules/fare.js new file mode 100644 index 00000000..be3c3c4d --- /dev/null +++ b/src/modules/fare.js @@ -0,0 +1,193 @@ +const axios = require("axios"); +const logger = require("./logger").default; + +const { naverMap } = require("@/loadenv"); +const { taxiFareModel, locationModel } = require("./stores/mongo"); + +const naverMapApi = { + "X-NCP-APIGW-API-KEY-ID": naverMap.apiId, + "X-NCP-APIGW-API-KEY": naverMap.apiKey, +}; + +// 30분 간격으로 하루를 48개의 시간대로 나누어 택시 요금을 계산합니다. +const timeConstants = 48; + +/** + * 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + * @param {Date} time: 시간 + * @returns {number} scaledTime + */ +const scaledTime = (time) => { + return ( + timeConstants * time.getDay() + + time.getHours() * 2 + + (time.getMinutes() >= 30 ? 1 : 0) + ); +}; + +/** + * 데이터베이스를 초기화합니다. 존재하지 않는 필드가 있을때, 기존의 값으로 초기화해 놓거나, 아얘 비어있을 경우에 api를 통해 값을 받아와 초기화합니다. + * @returns + */ +const initializeDatabase = async () => { + try { + if ( + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] + ) { + logger.error( + "There is no credential for Naver Map. Taxi Fare functions are disabled." + ); + return; + } + const location = await locationModel + .find({ isValid: { $eq: true } }) + .lean(); + + await Promise.all( + location.map(async (from) => { + return Promise.all( + location.map(async (to) => { + if (from._id === to._id) return; + let tableFare = []; + const prevTaxiFare = ( + await taxiFareModel + .findOne( + { + from: from._id, + to: to._id, + }, + { fare: true } + ) + .lean() + )?.fare; + const fare = prevTaxiFare + ? prevTaxiFare + : await callTaxiFare(from, to); + if ( + (from.koName === "카이스트 본원" && to.koName === "대전역") || + (from.koName === "대전역" && to.koName === "카이스트 본원") + ) { + [...Array(timeConstants * 7)].map((_, i) => { + tableFare.push({ + updateOne: { + filter: { + from: from._id, + to: to._id, + time: i, + isMajor: true, + }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, + }, + }); + }); + } else { + [...Array(7)].map((_, i) => { + tableFare.push({ + updateOne: { + filter: { + from: from._id, + to: to._id, + time: i * timeConstants, + isMajor: false, + }, + update: { + $setOnInsert: { + fare: fare, + }, + }, + upsert: true, + }, + }); + }); + } + await taxiFareModel.bulkWrite(tableFare); + await new Promise((resolve) => setTimeout(resolve, 200)); + }) + ); + }) + ); + } catch (err) { + logger.error("Error occured while initializing database: " + err.message); + } +}; + +/** + * 주어진 from, to, sTime에 대한 단일 택시 요금을 업데이트합니다. + * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, cron에 의해 매일 18:00시의 택시 요금을 업데이트 하게 됩니다. + * @summary 카이스트 본원 <-> 대전역의 경우, 미리 캐싱해놓은 데이터를 기반으로 주어진 시간(30분 간격)에 대한 택시 요금을 반환합니다. + * @param {number} sTime - 출발 시간 (scaledTime에 의해 변경된 시간, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + * @param {Boolean} isMajor - 카이스트 본원 <-> 대전역 경로 / 이외 경로 + */ +const updateTaxiFare = async (sTime, isMajor) => { + if ( + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] + ) { + logger.error( + "There is no credential for Naver Map. Taxi Fare functions are disabled." + ); + return; + } + const prevFares = await taxiFareModel + .find({ + time: sTime, + isMajor: isMajor, + }) + .lean(); + await prevFares.reduce(async (acc, item) => { + const from = await locationModel.findOne({ _id: item.from }); + const to = await locationModel.findOne({ _id: item.to }); + + await acc; + await callTaxiFare(from, to) + .then(async (fare) => { + if (fare) { + await taxiFareModel.updateOne( + { from: item.from, to: item.to, time: sTime }, + { fare: fare } + ); + } + }) + .catch((err) => { + logger.error(err.message); + }); + await new Promise((resolve) => setTimeout(() => resolve, 200)); + return acc; + }, Promise.resolve()); // 초기값 설정 안 하면, 처음에 acc가 undefined로 들어가서 첫 인덱스를 to에서 못 쓰게 됨 +}; + +/** + * @param {locationSchema} from : 출발지 (longitude, latitude) + * @param {locationSchema} to : 도착지 (longitude, latitude) + * @returns naver map api call을 통해 받아온 예상 택시 요금 + */ +const callTaxiFare = async (from, to) => { + if ( + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] + ) { + logger.error( + "There is no credential for Naver Map. Taxi Fare functions are disabled." + ); + return; + } + return ( + await axios.get( + `https://naveropenapi.apigw.ntruss.com/map-direction/v1/driving?start=${from.longitude},${from.latitude}}&goal=${to.longitude},${to.latitude}&options=traoptimal`, + { headers: naverMapApi } + ) + ).data.route.traoptimal[0].summary.taxiFare; +}; + +module.exports = { + scaledTime, + initializeDatabase, + updateTaxiFare, + callTaxiFare, +}; diff --git a/src/modules/fcm.js b/src/modules/fcm.ts similarity index 61% rename from src/modules/fcm.js rename to src/modules/fcm.ts index 7faaaad2..9f80c4f3 100644 --- a/src/modules/fcm.js +++ b/src/modules/fcm.ts @@ -1,17 +1,18 @@ -const firebaseAdmin = require("firebase-admin"); -const { getMessaging } = require("firebase-admin/messaging"); -const { +import firebaseAdmin from "firebase-admin"; +import { type SendResponse, getMessaging } from "firebase-admin/messaging"; +import { googleApplicationCredentials } from "@/loadenv"; +import logger from "@/modules/logger"; +import { deviceTokenModel, notificationOptionModel, topicSubscriptionModel, -} = require("./stores/mongo"); -const logger = require("../modules/logger"); -const { googleApplicationCredentials } = require("../../loadenv"); +} from "@/modules/stores/mongo"; +import type { ChatType } from "@/types/mongo"; /** * credential을 등록합니다. */ -const initializeApp = () => { +export const initializeApp = () => { if (googleApplicationCredentials) { firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.cert(googleApplicationCredentials), @@ -25,11 +26,14 @@ const initializeApp = () => { /** * 사용자의 ObjectId와 FCM device token이 주어졌을 때, 해당 deviceToken을 사용자의 토큰으로 DB에 등록합니다. - * @param {string} userId - 사용자의 ObjectId입니다. - * @param {string} deviceToken - 등록하려는 FCM device token입니다. - * @return {Promise>} 변경된 사용자의 deviceToken의 목록 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. + * @param userId - 사용자의 ObjectId입니다. + * @param deviceToken - 등록하려는 FCM device token입니다. + * @return 변경된 사용자의 deviceToken의 목록 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ -const registerDeviceToken = async (userId, deviceToken) => { +export const registerDeviceToken = async ( + userId: string, + deviceToken: string +) => { try { // 디바이스 토큰을 다른 사용자가 사용하고 있는지 확인 및 삭제합니다. await deviceTokenModel.updateMany( @@ -61,14 +65,12 @@ const registerDeviceToken = async (userId, deviceToken) => { } }; -// TODO: remove userId /** * 사용자의 ObjectId와 FCM device token이 주어졌을 때, 해당 사용자의 해당 deviceToken을 DB에서 삭제합니다. - * @param {string} userId - 사용자의 ObjectId입니다. - * @param {string} deviceToken - 삭제하려는 FCM device token입니다. - * @return {Promise} 해당 deviceToken을 가진 모든 사용자로부터 해당 deviceToken을 삭제하는 데 성공하면 true, 하나 이상의 사용자에게서 해당 deviceToken을 삭제하는 데 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. + * @param deviceToken - 삭제하려는 FCM device token입니다. + * @return 해당 deviceToken을 가진 모든 사용자로부터 해당 deviceToken을 삭제하는 데 성공하면 true, 하나 이상의 사용자에게서 해당 deviceToken을 삭제하는 데 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. */ -const unregisterDeviceToken = async (deviceToken) => { +export const unregisterDeviceToken = async (deviceToken: string) => { try { // 디바이스 토큰을 DB에서 삭제합니다. const { matchedCount, modifiedCount } = await deviceTokenModel.updateMany( @@ -93,10 +95,10 @@ const unregisterDeviceToken = async (deviceToken) => { /** * 사용자의 ObjectId가 주어졌을 때, 해당 사용자의 모든 deviceToken을 DB에서 삭제합니다. - * @param {string} userId - 사용자의 ObjectId입니다. - * @return {Promise} 해당 사용자로부터 deviceToken을 삭제하는 데 성공하면 true, 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. + * @param userId - 사용자의 ObjectId입니다. + * @return 해당 사용자로부터 deviceToken을 삭제하는 데 성공하면 true, 실패하면 false를 반환합니다. 삭제할 deviceToken이 존재하지 않는 경우에는 true를 반환합니다. */ -const unregisterAllDeviceTokens = async (userId) => { +export const unregisterAllDeviceTokens = async (userId: string) => { try { // 사용자의 디바이스 토큰을 DB에서 가져옵니다. // getTokensOfUsers 함수의 정의는 아래에 있습니다. (호이스팅) @@ -115,17 +117,20 @@ const unregisterAllDeviceTokens = async (userId) => { /** * 메시지 전송에 실패한 deviceToken을 DB에서 삭제합니다. - * @param {Array} deviceTokens - 사용자의 ObjectId입니다. - * @param {Array} fcmResponses - 등록하려는 FCM device token입니다. - * @return {Promise>} 각각의 토큰들의 삭제 성공 여부가 저장된 Array를 반환합니다. 해당 토큰을 DB에서 삭제하는 데 성공했으면 true, 아니면 false가 포함됩니다. + * @param deviceTokens - 사용자의 ObjectId입니다. + * @param fcmResponses - 등록하려는 FCM device token입니다. + * @return 각각의 토큰들의 삭제 성공 여부가 저장된 Array를 반환합니다. 해당 토큰을 DB에서 삭제하는 데 성공했으면 true, 아니면 false가 포함됩니다. */ -const removeExpiredTokens = async (deviceTokens, fcmResponses) => { +const removeExpiredTokens = async ( + deviceTokens: string[], + fcmResponses: SendResponse[] +) => { const removalResults = await Promise.all( deviceTokens.map(async (deviceToken, index) => { try { // FCM device token이 유효하지 않아 메시지 전송에 실패한 경우, 해당 device token을 DB에서 삭제합니다. if ( - fcmResponses[index].error.code === + fcmResponses[index].error?.code === "messaging/registration-token-not-registered" ) { await unregisterDeviceToken(deviceToken); @@ -148,10 +153,10 @@ const removeExpiredTokens = async (deviceTokens, fcmResponses) => { /** * 사용자의 FCM device token이 현재 사용 가능한지 검증합니다. * @summary 해당 디바이스에 dry-run 방식으로 메시지 전송을 시험함으로써 해당 deviceToken이 사용 가능한지 검증합니다. dry-run 시 FCM 서버에는 메시지 전송 요청이 전송되지만, 실제 기기에는 알림이 전송되지 않습니다. - * @param {string} deviceToken - 사용 가능 여부를 확인하려고 하는 FCM device token입니다. - * @return {Promise} 해당 디바이스에 알림을 보낸다는 요청을 FCM 서버에 성공적으로 보냈으면 true, 아니면 false를 반환합니다. + * @param deviceToken - 사용 가능 여부를 확인하려고 하는 FCM device token입니다. + * @return 해당 디바이스에 알림을 보낸다는 요청을 FCM 서버에 성공적으로 보냈으면 true, 아니면 false를 반환합니다. */ -const validateDeviceToken = async (deviceToken) => { +export const validateDeviceToken = async (deviceToken: string) => { try { const message = { token: deviceToken, @@ -169,12 +174,15 @@ const validateDeviceToken = async (deviceToken) => { /** * 사용자들의 ObjectId의 배열이 주어졌을 때, 해당 사용자들의 모든 deviceToken을 하나의 Array로 반환합니다. - * @param {Array} userIds - 사용자의 ObjectId로 이루어진 Array입니다. - * @param {Object?} notificationOptions - 특정 알림 설정을 비활성화한 사용자를 필터링하기 위해 사용되는 Object입니다. + * @param userIds - 사용자의 ObjectId로 이루어진 Array입니다. + * @param notificationOptions - 특정 알림 설정을 비활성화한 사용자를 필터링하기 위해 사용되는 Object입니다. * @param {Boolean?} notificationOptions.chatting - true 또는 false로 주어진 경우, 채팅 알림 설정이 각각 true 또는 false로 설정된 사용자들의 deviceToken만 반환합니다. - * @return {Promise>} deviceToken의 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. + * @return deviceToken의 Array를 반환합니다. 오류가 발생하면 빈 배열을 반환합니다. */ -const getTokensOfUsers = async (userIds, notificationOptions = {}) => { +export const getTokensOfUsers = async ( + userIds: string[], + notificationOptions: object = {} +) => { const deviceTokensOfUsers = ( await Promise.all( userIds.map( @@ -201,15 +209,22 @@ const getTokensOfUsers = async (userIds, notificationOptions = {}) => { /** * 주어진 token들에 메시지 알림을 전송합니다. * TODO: 알림 전송 실패한 토큰 삭제하기 - * @param {Array} tokens - 메시지 알림을 받을 기기의 deviceToken들로 구성된 Array입니다. - * @param {string} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. - * @param {string} title - 보낼 메시지의 제목입니다. - * @param {string} body - 보낼 메시지의 본문입니다. - * @param {string?} icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. - * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. - * @return {Promise} 메시지 알림 전송에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. + * @param tokens - 메시지 알림을 받을 기기의 deviceToken들로 구성된 Array입니다. + * @param type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. + * @param title - 보낼 메시지의 제목입니다. + * @param body - 보낼 메시지의 본문입니다. + * @param icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. + * @param link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. + * @return 메시지 알림 전송에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { +export const sendMessageByTokens = async ( + tokens: string[], + type: ChatType, + title: string, + body: string, + icon?: string, + link?: string +) => { if (tokens.length === 0) return -1; try { const message = { @@ -218,17 +233,16 @@ const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { title, body, url: link || "/", - icon: icon || "/icons-512.png", + icon: icon || "https://taxi.sparcs.org/icons-512.png", click_action: "FLUTTER_NOTIFICATION_CLICK", }, apns: { payload: { aps: { alert: { title, body } } } }, android: { - ttl: 0, + priority: "high" as const, }, }; - const { responses, failureCount } = await getMessaging().sendMulticast( - message - ); + const { responses, failureCount } = + await getMessaging().sendEachForMulticast(message); // 메시지 전송에 실패한 기기가 존재할 경우, 해당 기기의 deviceToken을 DB에서 삭제합니다. if (failureCount) { @@ -245,15 +259,22 @@ const sendMessageByTokens = async (tokens, type, title, body, icon, link) => { /** * 주어진 topic을 구독하고 있는 모든 기기에 메시지 알림을 전송합니다. - * @param {string} topic - 메시지 알림을 보낼 기기들이 구독하고 있는 topic입니다. - * @param {string} type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. - * @param {string} title - 보낼 메시지의 제목입니다. - * @param {string} body - 보낼 메시지의 본문입니다. - * @param {string?} icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. - * @param {string?} link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. - * @return {Promise} 메시지 알림 전송에 성공했으면 true, 아니면 false를 반환합니다. + * @param topic - 메시지 알림을 보낼 기기들이 구독하고 있는 topic입니다. + * @param type - 메시지 유형으로, "text" | "in" | "out" | "s3img" | "payment" | "settlement" 입니다. + * @param title - 보낼 메시지의 제목입니다. + * @param body - 보낼 메시지의 본문입니다. + * @param icon - 메시지를 보낸 사람의 프로필 사진 주소입니다. + * @param link - 메시지 알림 팝업을 클릭했을 때 이동할 주소입니다. + * @return 메시지 알림 전송에 성공했으면 true, 아니면 false를 반환합니다. */ -const sendMessageByTopic = async (topic, type, title, body, icon, link) => { +export const sendMessageByTopic = async ( + topic: string, + type: ChatType, + title: string, + body: string, + icon?: string, + link?: string +) => { try { const message = { topic, @@ -279,11 +300,11 @@ const sendMessageByTopic = async (topic, type, title, body, icon, link) => { /** * 주어진 사용자를 특정한 topic에 구독시킵니다. - * @param {string} userId - topic을 구독할 사용자의 ObjectId입니다. - * @param {string} topic - 구독할 topic입니다. - * @return {Promise} 토픽 구독에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. + * @param userId - topic을 구독할 사용자의 ObjectId입니다. + * @param topic - 구독할 topic입니다. + * @return 토픽 구독에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -const subscribeUserToTopic = async (userId, topic) => { +export const subscribeUserToTopic = async (userId: string, topic: string) => { try { const deviceToken = await deviceTokenModel.findOne({ userId, @@ -330,11 +351,14 @@ const subscribeUserToTopic = async (userId, topic) => { /** * 주어진 사용자를 특정한 topic으로부터 구독 해제시킵니다. - * @param {string} userId - topic을 구독 해제할 사용자의 id입니다. - * @param {string} topic - 구독을 해제할 topic입니다. - * @return {Promise} 토픽 구독 해제에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. + * @param userId - topic을 구독 해제할 사용자의 id입니다. + * @param topic - 구독을 해제할 topic입니다. + * @return 토픽 구독 해제에 실패한 기기의 수를 반환합니다. 오류가 발생하면 -1을 반환합니다. */ -const unsubscribeUserFromTopic = async (userId, topic) => { +export const unsubscribeUserFromTopic = async ( + userId: string, + topic: string +) => { try { const deviceToken = await deviceTokenModel.findOne({ userId, @@ -368,16 +392,3 @@ const unsubscribeUserFromTopic = async (userId, topic) => { return -1; } }; - -module.exports = { - initializeApp, - registerDeviceToken, - unregisterDeviceToken, - unregisterAllDeviceTokens, - validateDeviceToken, - getTokensOfUsers, - sendMessageByTokens, - sendMessageByTopic, - subscribeUserToTopic, - unsubscribeUserFromTopic, -}; diff --git a/src/modules/logger.js b/src/modules/logger.ts similarity index 93% rename from src/modules/logger.js rename to src/modules/logger.ts index e532e7aa..b4b91610 100644 --- a/src/modules/logger.js +++ b/src/modules/logger.ts @@ -1,8 +1,8 @@ -const path = require("path"); -const { createLogger, format, transports } = require("winston"); -const DailyRotateFileTransport = require("winston-daily-rotate-file"); +import path from "path"; +import { createLogger, format, transports } from "winston"; +import DailyRotateFileTransport from "winston-daily-rotate-file"; -const { nodeEnv } = require("../../loadenv"); +import { nodeEnv } from "@/loadenv"; // logger에서 사용할 포맷들을 정의합니다. const baseFormat = format.combine( @@ -92,4 +92,4 @@ const logger = exceptionHandlers: [consoleTransport], }); -module.exports = logger; +export default logger; diff --git a/src/modules/modifyProfile.js b/src/modules/modifyProfile.ts similarity index 83% rename from src/modules/modifyProfile.js rename to src/modules/modifyProfile.ts index e8702f98..a440e484 100755 --- a/src/modules/modifyProfile.js +++ b/src/modules/modifyProfile.ts @@ -1,5 +1,5 @@ -const crypto = require("crypto"); -const aws = require("./stores/aws"); +import crypto from "crypto"; +import { getS3Url } from "@/modules/stores/aws"; const nouns = [ "재료역학", @@ -64,7 +64,7 @@ const defaultProfile = [ // 닉네임 규칙에 따라 새 유저의 닉네임을 생성해 반환합니다. // Ara의 닉네임 생성 규칙을 참고하였습니다. -const generateNickname = (id) => { +export const generateNickname = (id: string) => { const nounIdx = crypto.randomInt(nouns.length); const adjectiveIdx = crypto.randomInt(adjectives.length); const noun = nouns[nounIdx]; @@ -80,26 +80,19 @@ const generateNickname = (id) => { }; // 기존 프로필 사진의 URI 중 하나를 무작위로 선택해 반환합니다. -const generateProfileImageUrl = () => { +export const generateProfileImageUrl = () => { const ridx = crypto.randomInt(defaultProfile.length); - return aws.getS3Url(`/profile-img/default/${defaultProfile[ridx]}`); + return getS3Url(`/profile-img/default/${defaultProfile[ridx]}`); }; // 사용자의 이름과 성을 받아, 한글인지 영어인지에 따라 전체 이름을 반환합니다. -const getFullUsername = (firstName, lastName) => { +export const getFullUsername = (firstName: string, lastName: string) => { const koPattern = new RegExp("[가-힣]+"); if (koPattern.test(firstName) && koPattern.test(lastName)) return `${lastName}${firstName}`; else return `${firstName} ${lastName}`; }; -const replaceSpaceInNickname = (nickname) => { +export const replaceSpaceInNickname = (nickname: string) => { return nickname.replace(/\s+/g, " "); }; - -module.exports = { - generateNickname, - generateProfileImageUrl, - getFullUsername, - replaceSpaceInNickname, -}; diff --git a/src/modules/patterns.js b/src/modules/patterns.ts similarity index 76% rename from src/modules/patterns.js rename to src/modules/patterns.ts index 2fd1e4dc..e01f748c 100644 --- a/src/modules/patterns.js +++ b/src/modules/patterns.ts @@ -1,4 +1,4 @@ -module.exports = { +export default { objectId: RegExp("^[a-fA-F\\d]{24}$"), room: { name: RegExp( @@ -15,5 +15,7 @@ module.exports = { chat: { chatImgType: RegExp("^(image/png|image/jpg|image/jpeg)$"), chatSendType: RegExp("^(text|account)$"), + chatContent: RegExp("^\\s{0,}\\S{1}[\\s\\S]{0,}$"), // 왼쪽 공백 제외 최소 1개 문자 + chatContentLength: RegExp("^[\\s\\S]{1,140}$"), // 공백 포함 최대 140문자 }, }; diff --git a/src/modules/populates/chats.js b/src/modules/populates/chats.js deleted file mode 100644 index 64a7fd90..00000000 --- a/src/modules/populates/chats.js +++ /dev/null @@ -1,13 +0,0 @@ -/** @constant {{path: string, select: string}[]} - * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. - */ -const chatPopulateOption = [ - { - path: "authorId", - select: "_id nickname profileImageUrl withdraw", - }, -]; - -module.exports = { - chatPopulateOption, -}; diff --git a/src/modules/populates/chats.ts b/src/modules/populates/chats.ts new file mode 100644 index 00000000..efb1b392 --- /dev/null +++ b/src/modules/populates/chats.ts @@ -0,0 +1,18 @@ +import type { User, Chat } from "@/types/mongo"; + +/** + * 쿼리를 통해 얻은 Chat Document를 populate할 설정값을 정의합니다. + */ +export const chatPopulateOption = [ + { + path: "authorId", + select: "_id nickname profileImageUrl withdraw", + }, +]; + +export interface PopulatedChat extends Omit { + authorId: Pick< + User, + "_id" | "nickname" | "profileImageUrl" | "withdraw" + > | null; +} diff --git a/src/modules/populates/reports.js b/src/modules/populates/reports.js deleted file mode 100644 index baa7b4ee..00000000 --- a/src/modules/populates/reports.js +++ /dev/null @@ -1,10 +0,0 @@ -const reportPopulateOption = [ - { - path: "reportedId", - select: "_id id name nickname profileImageUrl withdraw", - }, -]; - -module.exports = { - reportPopulateOption, -}; diff --git a/src/modules/populates/reports.ts b/src/modules/populates/reports.ts new file mode 100644 index 00000000..1d30a07e --- /dev/null +++ b/src/modules/populates/reports.ts @@ -0,0 +1,15 @@ +import type { User, Report } from "@/types/mongo"; + +export const reportPopulateOption = [ + { + path: "reportedId", + select: "_id id name nickname profileImageUrl withdraw", + }, +]; + +export interface PopulatedReport extends Omit { + reportedId: Pick< + User, + "_id" | "id" | "name" | "nickname" | "profileImageUrl" | "withdraw" + > | null; +} diff --git a/src/modules/populates/rooms.js b/src/modules/populates/rooms.js deleted file mode 100644 index a538a47f..00000000 --- a/src/modules/populates/rooms.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * 쿼리를 통해 얻은 Room Document를 populate할 설정값을 정의합니다. - * @constant {{path: string, select: string, populate?: {path: string, select: string}}[]} - */ -const roomPopulateOption = [ - { path: "from", select: "_id koName enName" }, - { path: "to", select: "_id koName enName" }, - { - path: "part", - select: "-_id user settlementStatus readAt", - populate: { - path: "user", - select: "_id id name nickname profileImageUrl withdraw", - }, - }, -]; - -/** - * Room Object가 주어졌을 때 room의 part array의 각 요소를 API 명세에서와 같이 {userId: String, ... , isSettlement: String}으로 가공합니다. - * 또한, 방이 현재 출발했는지 유무인 isDeparted 속성을 추가합니다. - * @param {Object} roomObject - 정산 정보를 가공할 room Object로, Mongoose Document가 아닌 순수 Javascript Object여야 합니다. - * @param {Object} options - 추가 파라미터로, 기본값은 {}입니다. - * @param {Boolean} options.includeSettlement - 반환 결과에 정산 정보를 포함할 지 여부로, 기본값은 true입니다. - * @param {Date} options.timestamp - 방의 출발 여부(isDeparted)를 판단하는 기준이 되는 시각입니다. - * @param {Boolean} options.isOver - 방의 완료 여부(isOver)로, 기본값은 false입니다. includeSettlement가 false인 경우 roomDocument의 isOver 속성은 undefined로 설정됩니다. - * @return {Object} 정산 여부가 위와 같이 가공되고 isDeparted 속성이 추가된 Room Object가 반환됩니다. - */ -const formatSettlement = ( - roomObject, - { includeSettlement = true, isOver = false, timestamp = Date.now() } = {} -) => { - roomObject.part = roomObject.part.map((participantSubDocument) => { - if (!participantSubDocument.user) return null; - - const { _id, name, nickname, profileImageUrl, withdraw } = - participantSubDocument.user; - const { settlementStatus, readAt } = participantSubDocument; - return { - _id, - name, - nickname, - profileImageUrl, - withdraw, - isSettlement: includeSettlement ? settlementStatus : undefined, - readAt: readAt ?? roomObject.madeat, - }; - }); - roomObject.settlementTotal = includeSettlement - ? roomObject.settlementTotal - : undefined; - roomObject.isOver = includeSettlement ? isOver : undefined; - roomObject.isDeparted = new Date(roomObject.time) < new Date(timestamp); - return roomObject; -}; - -/** - * formatSettlement 함수를 사용하여 변환한 Room Object와 사용자의 id(userId)가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. - * @param {Object} roomObject - formatSettlement 함수를 사용하여 변환한 Room Object입니다. - * @param {String} userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. - * @return {Boolean | undefined} 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. - **/ -const getIsOver = (roomObject, userId) => { - // room document의 part subdoocument에서 사용자 id와 일치하는 정산 정보를 찾습니다. - const participantSubDocuments = roomObject.part.filter((part) => { - return part.user.id === userId; - }); - - // 방에 참여중이지 않은 사용자의 경우, undefined을 반환합니다. - if (participantSubDocuments.length === 0) return undefined; - - // 방에 참여중인 사용자의 경우, 정산 상태가 완료된 것인지("paid"거나 "sent"인지)를 반환합니다. - return ["paid", "sent"].includes(participantSubDocuments[0].settlementStatus); -}; - -module.exports = { - roomPopulateOption, - formatSettlement, - getIsOver, -}; diff --git a/src/modules/populates/rooms.ts b/src/modules/populates/rooms.ts new file mode 100644 index 00000000..a63dc69a --- /dev/null +++ b/src/modules/populates/rooms.ts @@ -0,0 +1,132 @@ +import type { + User, + SettlementStatus, + Participant, + Room, + Location, +} from "@/types/mongo"; + +/** + * 쿼리를 통해 얻은 Room Document를 populate할 설정값을 정의합니다. + */ +export const roomPopulateOption = [ + { path: "from", select: "_id koName enName" }, + { path: "to", select: "_id koName enName" }, + { + path: "part", + select: "-_id user settlementStatus readAt", + populate: { + path: "user", + select: "_id id name nickname profileImageUrl withdraw", + }, + }, +]; + +interface PopulatedParticipant + extends Pick { + user: Pick< + User, + "_id" | "id" | "name" | "nickname" | "profileImageUrl" | "withdraw" + > | null; +} + +export interface PopulatedRoom extends Omit { + from: Pick | null; + to: Pick | null; + part: PopulatedParticipant[]; +} + +interface FormattedLocation { + _id: string; + enName: string; + koName: string; +} + +export interface FormattedRoom { + _id: string; + name: string; + from: FormattedLocation; + to: FormattedLocation; + time: Date; + madeat: Date; + maxPartLength: number; + part: { + _id: string; + name: string; + nickname: string; + profileImageUrl: string; + withdraw: boolean; + isSettlement?: SettlementStatus; + readAt: Date; + }[]; + settlementTotal?: number; + isOver?: boolean; + isDeparted: boolean; +} + +/** + * Room Object가 주어졌을 때 room의 part array의 각 요소를 API 명세에서와 같이 {userId: String, ... , isSettlement: String}으로 가공합니다. + * 또한, 방이 현재 출발했는지 유무인 isDeparted 속성을 추가합니다. + * @param roomObject - 정산 정보를 가공할 room Object로, Mongoose Document가 아닌 순수 Javascript Object여야 합니다. + * @param options - 추가 파라미터로, 기본값은 {}입니다. + * @param options.includeSettlement - 반환 결과에 정산 정보를 포함할 지 여부로, 기본값은 true입니다. + * @param options.timestamp - 방의 출발 여부(isDeparted)를 판단하는 기준이 되는 시각입니다. + * @param options.isOver - 방의 완료 여부(isOver)로, 기본값은 false입니다. includeSettlement가 false인 경우 roomDocument의 isOver 속성은 undefined로 설정됩니다. + * @return 정산 여부가 위와 같이 가공되고 isDeparted 속성이 추가된 Room Object가 반환됩니다. + */ +export const formatSettlement = ( + roomObject: PopulatedRoom, + { includeSettlement = true, isOver = false, timestamp = Date.now() } = {} +): FormattedRoom => { + return { + ...roomObject, + _id: roomObject._id!.toString(), + from: { + _id: roomObject.from!._id!.toString(), + enName: roomObject.from!.enName, + koName: roomObject.from!.koName, + }, + to: { + _id: roomObject.to!._id!.toString(), + enName: roomObject.to!.enName, + koName: roomObject.to!.koName, + }, + part: roomObject.part.map((participantSubDocument) => { + const { _id, name, nickname, profileImageUrl, withdraw } = + participantSubDocument.user!; + const { settlementStatus, readAt } = participantSubDocument; + return { + _id: _id!.toString(), + name, + nickname, + profileImageUrl, + withdraw, + isSettlement: includeSettlement ? settlementStatus : undefined, + readAt: readAt ?? roomObject.madeat, + }; + }), + settlementTotal: includeSettlement ? roomObject.settlementTotal : undefined, + isOver: includeSettlement ? isOver : undefined, + isDeparted: new Date(roomObject.time) < new Date(timestamp), + }; +}; + +/** + * roomPopulateOption을 사용해 populate된 Room Object와 사용자의 id(userId)가 주어졌을 때, 해당 사용자의 정산 상태를 반환합니다. + * @param roomObject - roomPopulateOption을 사용해 populate된 변환한 Room Object입니다. + * @param userId - 방 완료 상태를 확인하려는 사용자의 id(user.id)입니다. + * @return 사용자의 해당 방에 대한 완료 여부(true | false)를 반환합니다. 사용자가 참여중인 방이 아닐 경우 undefined를 반환합니다. + **/ +export const getIsOver = (roomObject: PopulatedRoom, userId: string) => { + // room document의 part subdoocument에서 사용자 id와 일치하는 정산 정보를 찾습니다. + const participantSubDocuments = roomObject.part?.filter((part) => { + return part.user?.id === userId; + }); + + // 방에 참여중이지 않은 사용자의 경우, undefined을 반환합니다. + if (!participantSubDocuments || participantSubDocuments.length === 0) + return undefined; + + // 방에 참여중인 사용자의 경우, 정산 상태가 완료된 것인지("paid"거나 "sent"인지)를 반환합니다. + return ["paid", "sent"].includes(participantSubDocuments[0].settlementStatus); +}; diff --git a/src/modules/slackNotification.js b/src/modules/slackNotification.ts similarity index 59% rename from src/modules/slackNotification.js rename to src/modules/slackNotification.ts index 544e8a31..c70cfdb6 100644 --- a/src/modules/slackNotification.js +++ b/src/modules/slackNotification.ts @@ -1,18 +1,19 @@ -const { nodeEnv, slackWebhookUrl: slackUrl } = require("../../loadenv"); -const axios = require("axios"); -const logger = require("../modules/logger"); +import axios from "axios"; +import { nodeEnv, slackWebhookUrl as slackUrl } from "@/loadenv"; +import logger from "@/modules/logger"; +import type { Report } from "@/types/mongo"; -const sendTextToReportChannel = (text) => { +export const sendTextToReportChannel = (text: string) => { if (!slackUrl.report) return; const data = { text: nodeEnv === "production" ? text : `(${nodeEnv}) ${text}`, // Production 환경이 아닌 경우, 환경 이름을 붙여서 전송합니다. }; - const config = { "Content-Type": "application/json" }; + const config = { headers: { "Content-Type": "application/json" } }; axios .post(slackUrl.report, data, config) - .then((res) => { + .then(() => { logger.info("Slack webhook sent successfully"); }) .catch((err) => { @@ -20,7 +21,10 @@ const sendTextToReportChannel = (text) => { }); }; -const notifyReportToReportChannel = (reportUser, report) => { +export const notifyReportToReportChannel = ( + reportUser: string, + report: Report +) => { sendTextToReportChannel( `${reportUser}님으로부터 신고가 접수되었습니다. @@ -32,10 +36,17 @@ const notifyReportToReportChannel = (reportUser, report) => { ); }; -const notifyRoomCreationAbuseToReportChannel = ( - abusingUser, - abusingUserNickname, - { from, to, time, maxPartLength } +interface RoomType { + from: string; + to: string; + time: Date; + maxPartLength: number; +} + +export const notifyRoomCreationAbuseToReportChannel = ( + abusingUser: string, + abusingUserNickname: string, + { from, to, time, maxPartLength }: RoomType ) => { sendTextToReportChannel( `${abusingUserNickname}님이 어뷰징이 의심되는 방을 생성하려고 시도했습니다. @@ -47,9 +58,3 @@ const notifyRoomCreationAbuseToReportChannel = ( 최대 참여 가능 인원: ${maxPartLength}명` ); }; - -module.exports = { - sendTextToReportChannel, - notifyReportToReportChannel, - notifyRoomCreationAbuseToReportChannel, -}; diff --git a/src/modules/socket.js b/src/modules/socket.js index 080f7814..b509d230 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -1,13 +1,13 @@ const { Server } = require("socket.io"); -const sessionMiddleware = require("../middlewares/session"); -const logger = require("./logger"); -const { getLoginInfo } = require("./auths/login"); -const { roomModel, userModel, chatModel } = require("./stores/mongo"); -const { getTokensOfUsers, sendMessageByTokens } = require("./fcm"); +const sessionMiddleware = require("@/middlewares/session").default; +const logger = require("@/modules/logger").default; +const { getLoginInfo } = require("@/modules/auths/login"); +const { roomModel, userModel, chatModel } = require("@/modules/stores/mongo"); +const { getTokensOfUsers, sendMessageByTokens } = require("@/modules/fcm"); -const { corsWhiteList } = require("../../loadenv"); -const { chatPopulateOption } = require("./populates/chats"); +const { corsWhiteList } = require("@/loadenv"); +const { chatPopulateOption } = require("@/modules/populates/chats"); /** * emitChatEvent의 필수 파라미터가 주어지지 않은 경우 발생하는 예외를 정의하는 클래스입니다. diff --git a/src/modules/stores/aws.js b/src/modules/stores/aws.ts similarity index 66% rename from src/modules/stores/aws.js rename to src/modules/stores/aws.ts index 60939ae3..4c1c6885 100644 --- a/src/modules/stores/aws.js +++ b/src/modules/stores/aws.ts @@ -1,8 +1,6 @@ -const { aws: awsEnv } = require("../../../loadenv"); +import AWS from "aws-sdk"; +import { aws as awsEnv } from "@/loadenv"; -const logger = require("../logger"); -// Load the AWS-SDK and s3 -const AWS = require("aws-sdk"); AWS.config.update({ region: "ap-northeast-2", signatureVersion: "v4", @@ -12,7 +10,10 @@ const s3 = new AWS.S3({ apiVersion: "2006-03-01" }); const ses = new AWS.SES({ apiVersion: "2010-12-01" }); // function to list Object -module.exports.getList = (directoryPath, cb) => { +export const getList = ( + directoryPath: string, + cb: (err: AWS.AWSError, data: AWS.S3.ListObjectsOutput) => void +) => { s3.listObjects( { Bucket: awsEnv.s3BucketName, @@ -25,7 +26,10 @@ module.exports.getList = (directoryPath, cb) => { }; // function to generate signed-url for upload(PUT) -module.exports.getUploadPUrlPut = (filePath, contentType = "image/png") => { +export const getUploadPUrlPut = ( + filePath: string, + contentType: string = "image/png" +) => { const presignedUrl = s3.getSignedUrl("putObject", { Bucket: awsEnv.s3BucketName, Key: filePath, @@ -36,7 +40,11 @@ module.exports.getUploadPUrlPut = (filePath, contentType = "image/png") => { }; // function to generate signed-url for upload(POST) -module.exports.getUploadPUrlPost = (filePath, contentType, cb) => { +export const getUploadPUrlPost = ( + filePath: string, + contentType: string, + cb: (err: Error, data: AWS.S3.PresignedPost) => void +) => { s3.createPresignedPost( { Bucket: awsEnv.s3BucketName, @@ -54,7 +62,10 @@ module.exports.getUploadPUrlPost = (filePath, contentType, cb) => { }; // function to delete object -module.exports.deleteObject = (filePath, cb) => { +export const deleteObject = ( + filePath: string, + cb: (err: AWS.AWSError, data: AWS.S3.DeleteObjectOutput) => void +) => { s3.deleteObject( { Bucket: awsEnv.s3BucketName, @@ -67,7 +78,10 @@ module.exports.deleteObject = (filePath, cb) => { }; // function to check exist of Object -module.exports.foundObject = (filePath, cb) => { +export const foundObject = ( + filePath: string, + cb: (err: AWS.AWSError, data: AWS.S3.HeadObjectOutput) => void +) => { s3.headObject( { Bucket: awsEnv.s3BucketName, @@ -80,6 +94,6 @@ module.exports.foundObject = (filePath, cb) => { }; // function to return full URL of the object -module.exports.getS3Url = (filePath) => { +export const getS3Url = (filePath: string) => { return `${awsEnv.s3Url}${filePath}`; }; diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.ts similarity index 62% rename from src/modules/stores/mongo.js rename to src/modules/stores/mongo.ts index 1e7194fe..6c162fe4 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.ts @@ -1,9 +1,22 @@ -const mongoose = require("mongoose"); -const Schema = mongoose.Schema; +import mongoose, { model, Schema, type Types } from "mongoose"; +import logger from "@/modules/logger"; +import type { + User, + Ban, + Participant, + DeviceToken, + NotificationOption, + TopicSubscription, + Room, + Location, + Chat, + Report, + AdminIPWhitelist, + AdminLog, + TaxiFare, +} from "@/types/mongo"; -const logger = require("../logger"); - -const userSchema = Schema({ +const userSchema = new Schema({ name: { type: String, required: true }, //실명 nickname: { type: String, required: true }, //닉네임 id: { type: String, required: true }, //택시 서비스에서만 사용되는 id @@ -27,40 +40,30 @@ const userSchema = Schema({ account: { type: String, default: "" }, //계좌번호 정보 }); -const banSchema = Schema({ +export const userModel = model("User", userSchema); + +const banSchema = new Schema({ // 정지 시킬 사용자를 기제함. - userId: { type: mongoose.Types.ObjectId, ref: "User", required: true }, + userSid: { type: String, required: true }, // 정지 사유 - reason: { + reason: { type: String, required: true }, + bannedAt: { type: Date, required: true }, // 정지 당한 시각 + expireAt: { type: Date, required: true }, // 정지 만료 시각 + // 정지를 당한 서비스를 기제함 + serviceName: { type: String, required: true, + // 필요시 이곳에 정지를 시킬 서비스를 추가함. + enum: [ + "service", // service: 방 생성/참여 제한 + "2023-fall-event", // xxxx-xxxx-event: 특정 이벤트 참여 제한 + ], }, - bannedAt: { - type: Date, // 정지 당한 시각 - required: true, - }, - expireAt: { - type: Date, // 정지 만료 시각 - required: true, - }, - services: [ - { - // 정지를 당한 서비스를 기제함 - serviceName: { - type: String, - required: true, - // 필요시 이곳에 정지를 시킬 서비스를 추가함. - enum: [ - "all", // all -> 과거/미래 모든 서비스 및 이벤트 이용 제한 - "service", // service -> 방 생성/참여 제한 - "2023-fall-event", // event -> 특정 이벤트 참여 제한 - ], - }, - }, - ], }); -const participantSchema = Schema({ +export const banModel = model("Ban", banSchema); + +const participantSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, settlementStatus: { type: String, @@ -71,7 +74,7 @@ const participantSchema = Schema({ readAt: { type: Date }, }); -const deviceTokenSchema = Schema({ +const deviceTokenSchema = new Schema({ userId: { type: Schema.Types.ObjectId, ref: "User", @@ -81,8 +84,10 @@ const deviceTokenSchema = Schema({ deviceTokens: [{ type: String, required: true }], }); +export const deviceTokenModel = model("DeviceToken", deviceTokenSchema); + // 각 디바이스의 알림 설정 -const notificationOptionSchema = Schema({ +const notificationOptionSchema = new Schema({ deviceToken: { type: String, required: true, @@ -116,7 +121,12 @@ const notificationOptionSchema = Schema({ }, //광고성 알림 수신 여부 }); -const topicSubscriptionSchema = Schema({ +export const notificationOptionModel = model( + "NotificationOption", + notificationOptionSchema +); + +const topicSubscriptionSchema = new Schema({ deviceToken: String, topic: String, subscribedAt: { @@ -126,7 +136,12 @@ const topicSubscriptionSchema = Schema({ }, }); -const roomSchema = Schema({ +export const topicSubscriptionModel = model( + "TopicSubscription", + topicSubscriptionSchema +); + +const roomSchema = new Schema({ name: { type: String, required: true, default: "이름 없음", text: true }, from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, @@ -134,26 +149,30 @@ const roomSchema = Schema({ part: { type: [participantSchema], validate: [ - function (value) { + function (this: Room, value: Types.DocumentArray) { return value.length <= this.maxPartLength; }, ], }, // 참여 멤버 및 정산 여부 madeat: { type: Date, required: true }, // 생성 날짜 settlementTotal: { type: Number, default: 0, required: true }, - maxPartLength: { type: Number, require: true, default: 4 }, + maxPartLength: { type: Number, required: true, default: 4 }, }); -const locationSchema = Schema({ +export const roomModel = model("Room", roomSchema); + +const locationSchema = new Schema({ enName: { type: String, required: true }, koName: { type: String, required: true }, priority: { type: Number, default: 0 }, isValid: { type: Boolean, default: true }, - latitude: { type: Number }, // 이후 required: true 로 수정 필요 - longitude: { type: Number }, // 이후 required: true 로 수정 필요 + latitude: { type: Number, required: true }, + longitude: { type: Number, required: true }, }); -const chatSchema = Schema({ +export const locationModel = model("Location", locationSchema); + +const chatSchema = new Schema({ roomId: { type: Schema.Types.ObjectId, ref: "Room", required: true }, type: { type: String, @@ -176,7 +195,9 @@ const chatSchema = Schema({ }); chatSchema.index({ roomId: 1, time: -1 }); -const reportSchema = Schema({ +export const chatModel = model("Chat", chatSchema); + +const reportSchema = new Schema({ creatorId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 신고한 사람 id reportedId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 신고받은 사람 id type: { @@ -189,12 +210,19 @@ const reportSchema = Schema({ roomId: { type: Schema.Types.ObjectId, ref: "Room" }, // 신고한 방 id }); -const adminIPWhitelistSchema = Schema({ +export const reportModel = model("Report", reportSchema); + +const adminIPWhitelistSchema = new Schema({ ip: { type: String, required: true }, // IP 주소 description: { type: String, default: "" }, // 설명 }); -const adminLogSchema = Schema({ +export const adminIPWhitelistModel = model( + "AdminIPWhitelist", + adminIPWhitelistSchema +); + +const adminLogSchema = new Schema({ user: { type: Schema.Types.ObjectId, ref: "User", required: true }, // Log 취급자 User time: { type: Date, required: true }, // Log 발생 시각 ip: { type: String, required: true }, // 접속 IP 주소 @@ -206,6 +234,23 @@ const adminLogSchema = Schema({ }, // 수행 업무 }); +export const adminLogModel = model("AdminLog", adminLogSchema); + +const taxiFareSchema = new Schema( + { + from: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 출발지 + to: { type: Schema.Types.ObjectId, ref: "Location", required: true }, // 도착지 + isMajor: { type: Boolean, default: false }, // 카이스트 본원 <-> 대전역 경로 여부 + time: { type: Number, required: true }, // 출발 시간 (24h를 30분 단위로 분리 & 요일 정보도 하나로 관리, 0 ~ 6 (Sunday~Saturday) * 48 + 0 ~ 47 (0:00 ~ 23:30)) + fare: { type: Number, default: 0 }, // 예상 택시 요금 + }, + { + timestamps: true, // 최근 업데이트 시간 기록용 + } +); + +export const taxiFareModel = model("TaxiFare", taxiFareSchema); + mongoose.set("strictQuery", true); const database = mongoose.connection; @@ -218,46 +263,26 @@ database.on("error", function (err) { mongoose.disconnect(); }); -const connectDatabase = (mongoUrl) => { - database.on("disconnected", function () { +export const connectDatabase = (mongoUrl: string) => { + database.on("disconnected", () => { // 데이터베이스 연결이 끊어지면 5초 후 재연결을 시도합니다. logger.error("Disconnected from database!"); setTimeout(() => { - mongoose.connect(mongoUrl, { + mongoose.connect( + mongoUrl /*{ useNewUrlParser: true, useUnifiedTopology: true, - }); + }*/ + ); // NOTE: https://velog.io/@untiring_dev/MongoDB-MongoDB-Mongoose%EC%97%90-%EC%97%B0%EA%B2%B0 }, 5000); }); - mongoose.connect(mongoUrl, { + mongoose.connect( + mongoUrl /*{ useNewUrlParser: true, useUnifiedTopology: true, - }); + }*/ + ); return database; }; - -module.exports = { - connectDatabase, - userModel: mongoose.model("User", userSchema), - banModel: mongoose.model("Ban", banSchema), - deviceTokenModel: mongoose.model("DeviceToken", deviceTokenSchema), - notificationOptionModel: mongoose.model( - "NotificationOption", - notificationOptionSchema - ), - topicSubscriptionModel: mongoose.model( - "TopicSubscription", - topicSubscriptionSchema - ), - roomModel: mongoose.model("Room", roomSchema), - locationModel: mongoose.model("Location", locationSchema), - chatModel: mongoose.model("Chat", chatSchema), - reportModel: mongoose.model("Report", reportSchema), - adminIPWhitelistModel: mongoose.model( - "AdminIPWhitelist", - adminIPWhitelistSchema - ), - adminLogModel: mongoose.model("AdminLog", adminLogSchema), -}; diff --git a/src/modules/stores/sessionStore.js b/src/modules/stores/sessionStore.ts similarity index 56% rename from src/modules/stores/sessionStore.js rename to src/modules/stores/sessionStore.ts index fca4da55..2eed4e2e 100644 --- a/src/modules/stores/sessionStore.js +++ b/src/modules/stores/sessionStore.ts @@ -1,20 +1,18 @@ -const expressSession = require("express-session"); -const redis = require("redis"); -const MongoStore = require("connect-mongo"); -const RedisStore = require("connect-redis")(expressSession); -const { - redis: redisUrl, - mongo: mongoUrl, - session: sessionConfig, -} = require("../../../loadenv"); -const logger = require("../logger"); +import MongoStore from "connect-mongo"; +import RedisStore from "connect-redis"; +import { createClient } from "redis"; +import { + redis as redisUrl, + mongo as mongoUrl, + session as sessionConfig, +} from "@/loadenv"; +import logger from "@/modules/logger"; -const getSessionStore = (redisUrl) => { +const getSessionStore = () => { // 환경변수 REDIS_PATH 유무에 따라 session 저장 방식이 변경됩니다. if (redisUrl) { - const client = redis.createClient({ + const client = createClient({ url: redisUrl, - legacyMode: true, }); // redis client 연결 성공 시 로그를 출력합니다. @@ -34,4 +32,4 @@ const getSessionStore = (redisUrl) => { } }; -module.exports = getSessionStore(redisUrl); +export default getSessionStore(); diff --git a/src/routes/admin.js b/src/routes/admin.js index 7cd28735..d1502a03 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -13,14 +13,15 @@ const { adminLogModel, deviceTokenModel, notificationOptionModel, -} = require("../modules/stores/mongo"); -const { buildResource } = require("../modules/adminResource"); + taxiFareModel, +} = require("@/modules/stores/mongo"); +const { buildResource } = require("@/modules/adminResource"); const router = express.Router(); // Requires admin property of the user to enter admin page. -router.use(require("../middlewares/authAdmin")); -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/authAdmin").default); +router.use(require("@/middlewares/auth").default); // Registration of the mongoose adapter AdminJS.registerAdapter(AdminJSMongoose); @@ -36,9 +37,10 @@ const resources = [ adminLogModel, deviceTokenModel, notificationOptionModel, + taxiFareModel, ] .map(buildResource()) - .concat(require("../lottery").resources); + .concat(/*require("@/lottery").resources*/ []); // Create router for admin page const adminJS = new AdminJS({ resources }); diff --git a/src/routes/auth.js b/src/routes/auth.js index 7b29a267..534814f6 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,14 +1,14 @@ const express = require("express"); const router = express.Router(); const { body, query } = require("express-validator"); -const validator = require("../middlewares/validator"); +const validator = require("@/middlewares/validator").default; -const authHandlers = require("../services/auth"); -const authReplaceHandlers = require("../services/auth.replace"); -const mobileAuthHandlers = require("../services/auth.mobile"); +const authHandlers = require("@/services/auth"); +const authReplaceHandlers = require("@/services/auth.replace"); +const mobileAuthHandlers = require("@/services/auth.mobile"); // 환경변수 SPARCSSSO_CLIENT_ID 유무에 따라 로그인 방식이 변경됩니다. -const { sparcssso: sparcsssoEnv } = require("../../loadenv"); +const { sparcssso: sparcsssoEnv } = require("@/loadenv"); const isAuthReplace = !sparcsssoEnv?.id; // 로그인 페이지로 redirect합니다. diff --git a/src/routes/chats.js b/src/routes/chats.js index f689348c..32b01329 100644 --- a/src/routes/chats.js +++ b/src/routes/chats.js @@ -1,13 +1,15 @@ const express = require("express"); const { body } = require("express-validator"); -const validator = require("../middlewares/validator"); -const patterns = require("../modules/patterns"); +const validator = require("@/middlewares/validator").default; +const patterns = require("@/modules/patterns").default; +const { validateBody } = require("@/middlewares/zod"); +const { chatsZod } = require("./docs/schemas/chatsSchema"); const router = express.Router(); -const chatsHandlers = require("../services/chats"); +const chatsHandlers = require("@/services/chats"); // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth").default); /** * 가장 최근에 도착한 60개의 채팅을 가져옵니다. @@ -47,10 +49,7 @@ router.post( */ router.post( "/send", - body("roomId").isMongoId(), - body("type").matches(patterns.chat.chatSendType), - body("content").isString(), - validator, + validateBody(chatsZod.sendChatHandler), chatsHandlers.sendChatHandler ); diff --git a/src/routes/docs.js b/src/routes/docs.js index 97a1b288..0a571d8e 100644 --- a/src/routes/docs.js +++ b/src/routes/docs.js @@ -1,6 +1,6 @@ const express = require("express"); const swaggerUi = require("swagger-ui-express"); -const swaggerDocs = require("./docs/swaggerDocs.js"); +const swaggerDocs = require("./docs/swaggerDocs"); const router = express.Router(); router.use(swaggerUi.serve); diff --git a/src/routes/docs/auth.replace.js b/src/routes/docs/auth.replace.js index 8246836b..be8b8174 100644 --- a/src/routes/docs/auth.replace.js +++ b/src/routes/docs/auth.replace.js @@ -1,4 +1,4 @@ -const loginReplacePage = require("../../views/loginReplacePage"); +const loginReplacePage = require("../../views/loginReplacePage").default; const tag = "auth"; const apiPrefix = "/auth(dev)"; diff --git a/src/routes/docs/chats.js b/src/routes/docs/chats.js index f9107201..3f4b63a3 100644 --- a/src/routes/docs/chats.js +++ b/src/routes/docs/chats.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("@/modules/patterns").default; const tag = "chats"; const apiPrefix = "/chats"; diff --git a/src/routes/docs/fare.js b/src/routes/docs/fare.js new file mode 100644 index 00000000..62fbfa50 --- /dev/null +++ b/src/routes/docs/fare.js @@ -0,0 +1,54 @@ +const { objectIdPattern } = require("./utils"); + +const tag = "fare"; +const apiPrefix = "/fare"; + +const fareDocs = {}; + +fareDocs[`${apiPrefix}/getTaxiFare`] = { + get: { + tags: [tag], + summary: "예상 택시 요금 반환", + description: + "start, goal, time에 따라 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다.", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + from: { type: "string", pattern: objectIdPattern }, + to: { type: "string", pattern: objectIdPattern }, + time: { type: "string", format: "date-time" }, + }, + }, + }, + }, + }, + responses: { + 200: { + description: "예상 택시 요금 반환 성공", + content: { + "application/json": { + schema: { + type: "object", + properties: { + fare: { type: "number", example: 10000 }, + }, + }, + }, + }, + }, + 500: { + description: "fare/getTaxiFareHandler: Failed to load taxi fare", + content: { + "text/html": { + example: "fare/getTaxiFareHandler: Failed to load taxi fare", + }, + }, + }, + }, + }, +}; + +module.exports = fareDocs; diff --git a/src/routes/docs/logininfo.js b/src/routes/docs/logininfo.js index 7c447760..3f9c0b0b 100644 --- a/src/routes/docs/logininfo.js +++ b/src/routes/docs/logininfo.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("@/modules/patterns").default; const tag = "logininfo"; const apiPrefix = "/logininfo"; diff --git a/src/routes/docs/reports.js b/src/routes/docs/reports.js index a11933ee..bfc29687 100644 --- a/src/routes/docs/reports.js +++ b/src/routes/docs/reports.js @@ -1,4 +1,4 @@ -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("@/modules/patterns").default; const tag = "reports"; const apiPrefix = "/reports"; diff --git a/src/routes/docs/rooms.js b/src/routes/docs/rooms.js index 3ac66e6b..41d6a1cb 100644 --- a/src/routes/docs/rooms.js +++ b/src/routes/docs/rooms.js @@ -1,4 +1,4 @@ -const { objectId, room } = require("../../modules/patterns"); +const { objectId, room } = require("@/modules/patterns").default; const tag = "rooms"; const apiPrefix = "/rooms"; @@ -72,6 +72,12 @@ roomsDocs[`${apiPrefix}/create`] = { }, }, examples: { + "방 생성 기능이 정지당한 경우": { + value: { + error: + "Rooms/join : user monday is temporarily restricted from creating rooms until 2024-08-23 15:00:00.", + }, + }, "출발지와 도착지가 같음": { value: { error: "Rooms/create : locations are same", @@ -309,6 +315,12 @@ roomsDocs[`${apiPrefix}/join`] = { }, }, examples: { + "방 참여 기능이 정지당한 경우": { + value: { + error: + "Rooms/join : user monday is temporarily restricted from joining rooms until 2024-08-23 15:00:00.", + }, + }, "사용자가 참여하는 진행 중 방이 5개 이상": { value: { error: "Rooms/join : participating in too many rooms", diff --git a/src/routes/docs/schemas/chatsSchema.js b/src/routes/docs/schemas/chatsSchema.js new file mode 100644 index 00000000..5f1d534c --- /dev/null +++ b/src/routes/docs/schemas/chatsSchema.js @@ -0,0 +1,15 @@ +const { z } = require("zod"); +const { zodToSchemaObject } = require("../utils"); +const { objectId, chat } = require("@/modules/patterns").default; + +const chatsZod = { + sendChatHandler: z.object({ + roomId: z.string().regex(objectId), + type: z.string().regex(chat.chatSendType), + content: z.string().regex(chat.chatContent).regex(chat.chatContentLength), + }), +}; + +const chatsSchema = zodToSchemaObject(chatsZod); + +module.exports = { chatsZod, chatsSchema }; diff --git a/src/routes/docs/schemas/fareSchema.js b/src/routes/docs/schemas/fareSchema.js new file mode 100644 index 00000000..5caa87e7 --- /dev/null +++ b/src/routes/docs/schemas/fareSchema.js @@ -0,0 +1,14 @@ +const { z } = require("zod"); +const { zodToSchemaObject } = require("../utils"); +const { objectId } = require("../../../modules/patterns").default; + +const fareZod = { + getTaxiFareHandler: z.object({ + from: z.string().regex(objectId), + to: z.string().regex(objectId), + time: z.string().datetime(), + }), +}; +const fareSchema = zodToSchemaObject(fareZod); + +module.exports = { fareSchema, fareZod }; diff --git a/src/routes/docs/schemas/reportsSchema.js b/src/routes/docs/schemas/reportsSchema.js index d208dbb7..0e4c43b9 100644 --- a/src/routes/docs/schemas/reportsSchema.js +++ b/src/routes/docs/schemas/reportsSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../utils"); -const { objectId } = require("../../../modules/patterns"); +const { objectId } = require("../../../modules/patterns").default; const reportsZod = { createHandler: z diff --git a/src/routes/docs/schemas/roomsSchema.js b/src/routes/docs/schemas/roomsSchema.js index a0af753e..c580571a 100644 --- a/src/routes/docs/schemas/roomsSchema.js +++ b/src/routes/docs/schemas/roomsSchema.js @@ -1,6 +1,6 @@ const { z } = require("zod"); const { zodToSchemaObject } = require("../utils"); -const { objectId, room } = require("../../../modules/patterns"); +const { objectId, room } = require("../../../modules/patterns").default; const roomsZod = {}; roomsZod["part"] = z diff --git a/src/routes/docs/swaggerDocs.js b/src/routes/docs/swaggerDocs.js index 62639dfa..35aabcf2 100644 --- a/src/routes/docs/swaggerDocs.js +++ b/src/routes/docs/swaggerDocs.js @@ -1,5 +1,7 @@ const { reportsSchema } = require("./schemas/reportsSchema"); const { roomsSchema } = require("./schemas/roomsSchema"); +const { fareSchema } = require("./schemas/fareSchema"); +const { chatsSchema } = require("./schemas/chatsSchema"); const reportsDocs = require("./reports"); const logininfoDocs = require("./logininfo"); const locationsDocs = require("./locations"); @@ -8,7 +10,8 @@ const authReplaceDocs = require("./auth.replace"); const usersDocs = require("./users"); const roomsDocs = require("./rooms"); const chatsDocs = require("./chats"); -const { port, nodeEnv } = require("../../../loadenv"); +const fareDocs = require("./fare"); +const { port, nodeEnv } = require("@/loadenv"); const serverList = [ { @@ -68,6 +71,10 @@ const swaggerDocs = { name: "chats", description: "채팅 시 발생하는 이벤트 정리", }, + { + name: "fare", + description: "예상 택시 금액 계산", + }, ], consumes: ["application/json"], produces: ["application/json"], @@ -80,11 +87,14 @@ const swaggerDocs = { ...authReplaceDocs, ...chatsDocs, ...roomsDocs, + ...fareDocs, }, components: { schemas: { ...reportsSchema, ...roomsSchema, + ...fareSchema, + ...chatsSchema, }, }, }; diff --git a/src/routes/docs/users.js b/src/routes/docs/users.js index e7d6e989..e477bf5b 100644 --- a/src/routes/docs/users.js +++ b/src/routes/docs/users.js @@ -1,6 +1,6 @@ const tag = "users"; const apiPrefix = "/users"; -const { objectId } = require("../../modules/patterns"); +const { objectId } = require("../../modules/patterns").default; const usersDocs = {}; usersDocs[`${apiPrefix}/agreeOnTermsOfService`] = { @@ -330,7 +330,7 @@ usersDocs[`${apiPrefix}/resetProfileImg`] = { }, }; -usersDocs[`${apiPrefix}/isBanned`] = { +usersDocs[`${apiPrefix}/getBanRecord`] = { get: { tags: [tag], summary: "본인의 현재 정지 기록을 가져움", @@ -344,10 +344,10 @@ usersDocs[`${apiPrefix}/isBanned`] = { type: "array", items: { properties: { - userId: { + userSid: { type: "string", - description: "사용자의 ObjectId", - pattern: objectId.source, + description: "사용자의 SSO ID", + pattern: "monday-sid", }, reason: { type: "string", @@ -364,85 +364,10 @@ usersDocs[`${apiPrefix}/isBanned`] = { description: "정지 만료 시각", example: "2024-05-21 12:00", }, - services: { - type: "array", - items: { - properties: { - serviceName: { - type: "string", - description: "정지를 당한 서비스 또는 이벤트 이름", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - 400: { - content: { - "text/html": { - example: "Users/isBanned : there is no ban record", - }, - }, - }, - 500: { - content: { - "text/html": { - example: "Users/isBanned : internal server error", - }, - }, - }, - }, - }, -}; - -usersDocs[`${apiPrefix}/getBanRecord`] = { - get: { - tags: [tag], - summary: "본인의 모든 정지 기록을 가져움", - description: - "정지 기록들 중 본인인 경우에 해당하는 정지 기록을 모두 가져옴", - responses: { - 200: { - content: { - "application/json": { - schema: { - type: "array", - items: { - properties: { - userId: { - type: "string", - description: "사용자의 ObjectId", - pattern: objectId.source, - }, - reason: { + serviceName: { type: "string", - description: "정지 사유", - example: "미정산", - }, - bannedAt: { - type: "date", - description: "정지 당한 시각", - example: "2024-05-20 12:00", - }, - expireAt: { - type: "date", - description: "정지 만료 시각", - example: "2024-05-21 12:00", - }, - services: { - type: "array", - items: { - properties: { - serviceName: { - type: "string", - description: "정지를 당한 서비스 또는 이벤트 이름", - }, - }, - }, + description: "정지를 당한 서비스 또는 이벤트 이름", + example: "2023-fall-event", }, }, }, diff --git a/src/routes/docs/utils.js b/src/routes/docs/utils.js index 2f99c13c..bd006150 100644 --- a/src/routes/docs/utils.js +++ b/src/routes/docs/utils.js @@ -1,5 +1,5 @@ const { zodToJsonSchema } = require("zod-to-json-schema"); -const logger = require("../../modules/logger"); +const logger = require("../../modules/logger").default; const zodToSchemaObject = (zodObejct) => { try { diff --git a/src/routes/fare.js b/src/routes/fare.js new file mode 100644 index 00000000..0cbe37c3 --- /dev/null +++ b/src/routes/fare.js @@ -0,0 +1,15 @@ +const express = require("express"); + +const { validateQuery } = require("../middlewares/zod"); +const { fareZod } = require("./docs/schemas/fareSchema"); +const { getTaxiFareHandler } = require("../services/fare"); + +const router = express.Router(); + +router.get( + "/getTaxiFare", + validateQuery(fareZod.getTaxiFareHandler), + getTaxiFareHandler +); + +module.exports = router; diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 00000000..83bc81dc --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,11 @@ +export { default as adminRouter } from "./admin"; +export { default as authRouter } from "./auth"; +export { default as chatRouter } from "./chats"; +export { default as docsRouter } from "./docs"; +export { default as fareRouter } from "./fare"; +export { default as locationRouter } from "./locations"; +export { default as logininfoRouter } from "./logininfo"; +export { default as notificationRouter } from "./notifications"; +export { default as reportRouter } from "./reports"; +export { default as roomRouter } from "./rooms"; +export { default as userRouter } from "./users"; diff --git a/src/routes/locations.js b/src/routes/locations.js index 7724d371..20b16ada 100644 --- a/src/routes/locations.js +++ b/src/routes/locations.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const locationsHandlers = require("../services/locations"); +const locationsHandlers = require("@/services/locations"); router.get("/", locationsHandlers.getAllLocationsHandler); diff --git a/src/routes/logininfo.js b/src/routes/logininfo.js index a3d9d4f7..ab8c32e9 100644 --- a/src/routes/logininfo.js +++ b/src/routes/logininfo.js @@ -1,7 +1,7 @@ const express = require("express"); const router = express.Router(); -const logininfoHandlers = require("../services/logininfo"); +const logininfoHandlers = require("@/services/logininfo"); router.route("/").get(logininfoHandlers.logininfoHandler); diff --git a/src/routes/notifications.js b/src/routes/notifications.js index dd666953..c27c22b3 100644 --- a/src/routes/notifications.js +++ b/src/routes/notifications.js @@ -1,12 +1,12 @@ const express = require("express"); const router = express.Router(); -const { query, body } = require("express-validator"); +const { body } = require("express-validator"); -const notificationHandlers = require("../services/notifications"); -const validator = require("../middlewares/validator"); +const notificationHandlers = require("@/services/notifications"); +const validator = require("@/middlewares/validator").default; // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth").default); // FCM 토큰 등록 router.post( diff --git a/src/routes/reports.js b/src/routes/reports.js index 3f1781b6..4d76e2f2 100644 --- a/src/routes/reports.js +++ b/src/routes/reports.js @@ -1,11 +1,11 @@ const express = require("express"); -const { validateBody } = require("../middlewares/zod"); +const { validateBody } = require("@/middlewares/zod"); const { reportsZod } = require("./docs/schemas/reportsSchema"); const router = express.Router(); -const reportHandlers = require("../services/reports"); +const reportHandlers = require("@/services/reports"); // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth").default); router.post( "/create", diff --git a/src/routes/rooms.js b/src/routes/rooms.js index 6345fa6f..f08b35eb 100644 --- a/src/routes/rooms.js +++ b/src/routes/rooms.js @@ -4,9 +4,9 @@ const { validateBody } = require("../middlewares/zod"); const { roomsZod } = require("./docs/schemas/roomsSchema"); const router = express.Router(); -const roomHandlers = require("../services/rooms"); -const validator = require("../middlewares/validator"); -const patterns = require("../modules/patterns"); +const roomHandlers = require("@/services/rooms"); +const validator = require("@/middlewares/validator").default; +const patterns = require("@/modules/patterns").default; // 조건(이름, 출발지, 도착지, 날짜)에 맞는 방들을 모두 반환한다. router.get( @@ -33,7 +33,10 @@ router.get( ); // 이후 API 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(require("@/middlewares/auth").default); + +// 방 생성/참여전 ban 여부 확인 +router.use(require("@/middlewares/ban").default); // 특정 id 방 세부사항 보기 router.get( diff --git a/src/routes/users.js b/src/routes/users.ts similarity index 74% rename from src/routes/users.js rename to src/routes/users.ts index b5d8be38..c5afa7a8 100755 --- a/src/routes/users.js +++ b/src/routes/users.ts @@ -1,15 +1,15 @@ -const express = require("express"); -const { body } = require("express-validator"); -const validator = require("../middlewares/validator"); -const patterns = require("../modules/patterns"); +import express from "express"; +import { body } from "express-validator"; +import { authMiddleware, validatorMiddleware } from "@/middlewares"; +import patterns from "@/modules/patterns"; const router = express.Router(); -const userHandlers = require("../services/users"); +import * as userHandlers from "@/services/users"; -const { replaceSpaceInNickname } = require("../modules/modifyProfile"); +import { replaceSpaceInNickname } from "@/modules/modifyProfile"; // 라우터 접근 시 로그인 필요 -router.use(require("../middlewares/auth")); +router.use(authMiddleware); // 이용 약관에 동의합니다. router.post( @@ -27,7 +27,7 @@ router.post( body("nickname") .customSanitizer(replaceSpaceInNickname) .matches(patterns.user.nickname), - validator, + validatorMiddleware, userHandlers.editNicknameHandler ); @@ -38,7 +38,7 @@ router.get("/resetNickname", userHandlers.resetNicknameHandler); router.post( "/editAccount", body("account").matches(patterns.user.account), - validator, + validatorMiddleware, userHandlers.editAccountHandler ); @@ -46,7 +46,7 @@ router.post( router.post( "/editProfileImg/getPUrl", body("type").matches(patterns.user.profileImgType), - validator, + validatorMiddleware, userHandlers.editProfileImgGetPUrlHandler ); @@ -56,13 +56,10 @@ router.get("/editProfileImg/done", userHandlers.editProfileImgDoneHandler); // 프로필 이미지를 기본값으로 재설정합니다. router.get("/resetProfileImg", userHandlers.resetProfileImgHandler); -// 유저의 현재 유효한 서비스 정지 기록들만 반환합니다. -router.get("/isBanned", userHandlers.isBannedHandler); - // 유저의 서비스 정지 기록들을 모두 반환합니다. router.get("/getBanRecord", userHandlers.getBanRecordHandler); // 회원 탈퇴를 요청합니다. router.post("/withdraw", userHandlers.withdrawHandler); -module.exports = router; +export default router; diff --git a/src/sampleGenerator/.gitignore b/src/sampleGenerator/.gitignore deleted file mode 100644 index 2909449b..00000000 --- a/src/sampleGenerator/.gitignore +++ /dev/null @@ -1,107 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - -# MongoDB Dump -dump/ diff --git a/src/sampleGenerator/index.js b/src/sampleGenerator/index.js index e83a9335..38481739 100644 --- a/src/sampleGenerator/index.js +++ b/src/sampleGenerator/index.js @@ -9,8 +9,7 @@ const { mongo: mongoUrl, numberOfChats, numberOfRooms } = require("./loadenv"); const database = connectDatabase(mongoUrl); -const fs = require("fs"); -const sampleData = JSON.parse(fs.readFileSync("./sampleData.json")); +const sampleData = require("./sampleData.json"); const main = async () => { await database.db.dropDatabase(); diff --git a/src/sampleGenerator/loadenv.js b/src/sampleGenerator/loadenv.js index 0843789b..d248ee91 100644 --- a/src/sampleGenerator/loadenv.js +++ b/src/sampleGenerator/loadenv.js @@ -1,5 +1,5 @@ // Root directory에 있는 .env.test 파일을 읽어옴 -require("dotenv").config({ path: "../../.env.test" }); +require("dotenv").config({ path: "./.env.test" }); module.exports = { mongo: process.env.DB_PATH, // required diff --git a/src/sampleGenerator/sampleData.json b/src/sampleGenerator/sampleData.json index 546812cd..b2e84816 100644 --- a/src/sampleGenerator/sampleData.json +++ b/src/sampleGenerator/sampleData.json @@ -45,8 +45,8 @@ { "koName": "대전복합터미널", "enName": "Daejeon Terminal Complex", - "longitude": 127.350161, - "latitude": 36.362785 + "longitude": 127.436880, + "latitude": 36.349766 }, { "koName": "만년중학교", diff --git a/src/schedules/index.js b/src/schedules/index.js deleted file mode 100644 index 0358217a..00000000 --- a/src/schedules/index.js +++ /dev/null @@ -1,9 +0,0 @@ -const cron = require("node-cron"); - -const registerSchedules = (app) => { - cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); - cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); - cron.schedule("0 0 1 * *", require("./deleteUserInfo")); -}; - -module.exports = registerSchedules; diff --git a/src/schedules/index.ts b/src/schedules/index.ts new file mode 100644 index 00000000..4cbabe34 --- /dev/null +++ b/src/schedules/index.ts @@ -0,0 +1,23 @@ +import type { Express } from "express"; +import cron from "node-cron"; +import { naverMap } from "@/loadenv"; + +import notifyBeforeDepart from "./notifyBeforeDepart"; +import notifyAfterArrival from "./notifyAfterArrival"; +import updateMajorTaxiFare from "./updateMajorTaxiFare"; +import updateMinorTaxiFare from "./updateMinorTaxiFare"; +import deleteUserInfo from "./deleteUserInfo"; + +const registerSchedules = (app: Express) => { + cron.schedule("*/5 * * * *", notifyBeforeDepart(app)); + cron.schedule("*/10 * * * *", notifyAfterArrival(app)); + + if (naverMap.apiId && naverMap.apiKey) { + cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app)); + cron.schedule("0 18 * * *", updateMinorTaxiFare(app)); + } + + cron.schedule("0 0 1 * *", deleteUserInfo); +}; + +export default registerSchedules; diff --git a/src/schedules/notifyAfterArrival.js b/src/schedules/notifyAfterArrival.js index dc3b668b..31c0b354 100644 --- a/src/schedules/notifyAfterArrival.js +++ b/src/schedules/notifyAfterArrival.js @@ -1,6 +1,6 @@ -const { roomModel, chatModel } = require("../modules/stores/mongo"); -const { emitChatEvent } = require("../modules/socket"); -const logger = require("../modules/logger"); +const { roomModel, chatModel } = require("@/modules/stores/mongo"); +const { emitChatEvent } = require("@/modules/socket"); +const logger = require("@/modules/logger").default; const MS_PER_MINUTE = 60000; diff --git a/src/schedules/notifyBeforeDepart.js b/src/schedules/notifyBeforeDepart.js index ffe66386..b1b6e87d 100644 --- a/src/schedules/notifyBeforeDepart.js +++ b/src/schedules/notifyBeforeDepart.js @@ -1,6 +1,6 @@ -const { roomModel, chatModel } = require("../modules/stores/mongo"); -const { emitChatEvent } = require("../modules/socket"); -const logger = require("../modules/logger"); +const { roomModel, chatModel } = require("@/modules/stores/mongo"); +const { emitChatEvent } = require("@/modules/socket"); +const logger = require("@/modules/logger").default; const MS_PER_MINUTE = 60000; diff --git a/src/schedules/updateMajorTaxiFare.js b/src/schedules/updateMajorTaxiFare.js new file mode 100644 index 00000000..68de1a3d --- /dev/null +++ b/src/schedules/updateMajorTaxiFare.js @@ -0,0 +1,14 @@ +const logger = require("../modules/logger").default; + +const { scaledTime, updateTaxiFare } = require("../modules/fare"); + +/* 카이스트 본원<-> 대전역 경로에 대한 택시 요금을 매 30분간격(매시 0분과 30분)으로 1주일 단위 캐싱합니다. */ +module.exports = (app) => async () => { + try { + const time = new Date(); + const sTime = scaledTime(time); + await updateTaxiFare(sTime, true); + } catch (err) { + logger.error(err); + } +}; diff --git a/src/schedules/updateMinorTaxiFare.js b/src/schedules/updateMinorTaxiFare.js new file mode 100644 index 00000000..5ef9d8a4 --- /dev/null +++ b/src/schedules/updateMinorTaxiFare.js @@ -0,0 +1,13 @@ +const logger = require("../modules/logger").default; + +const { updateTaxiFare } = require("../modules/fare"); + +/* 카이스트 본원<-> 대전역 경로 외의 238개 경로에 대한 택시 요금을 매일 18:00시에 1주일 단위로 캐싱합니다. */ +module.exports = (app) => async () => { + try { + const date = new Date(); + await updateTaxiFare(48 * date.getDay(), false); // 18:00시의 택시 요금이지만 db에는 48*(요일) + 0으로 저장됨 + } catch (err) { + logger.error(err); + } +}; diff --git a/src/services/auth.js b/src/services/auth.js index 225276da..c047cfd8 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -1,23 +1,19 @@ -const { - sparcssso: sparcsssoEnv, - nodeEnv, - testAccounts, -} = require("../../loadenv"); -const { userModel } = require("../modules/stores/mongo"); -const { user: userPattern } = require("../modules/patterns"); -const { getLoginInfo, logout, login } = require("../modules/auths/login"); - -const { unregisterDeviceToken } = require("../modules/fcm"); +const { sparcssso: sparcsssoEnv, nodeEnv, testAccounts } = require("@/loadenv"); +const { userModel } = require("@/modules/stores/mongo"); +const { user: userPattern } = require("@/modules/patterns").default; +const { getLoginInfo, logout, login } = require("@/modules/auths/login"); + +const { unregisterDeviceToken } = require("@/modules/fcm"); const { generateNickname, generateProfileImageUrl, getFullUsername, -} = require("../modules/modifyProfile"); -const jwt = require("../modules/auths/jwt"); -const logger = require("../modules/logger"); +} = require("@/modules/modifyProfile"); +const jwt = require("@/modules/auths/jwt"); +const logger = require("@/modules/logger").default; // SPARCS SSO -const Client = require("../modules/auths/sparcssso"); +const Client = require("@/modules/auths/sparcssso"); const client = new Client(sparcsssoEnv?.id, sparcsssoEnv?.key); const transUserData = (userData) => { diff --git a/src/services/auth.mobile.js b/src/services/auth.mobile.js index 6e373f43..e2cee5bb 100644 --- a/src/services/auth.mobile.js +++ b/src/services/auth.mobile.js @@ -1,14 +1,11 @@ -const { userModel } = require("../modules/stores/mongo"); -const { login } = require("../modules/auths/login"); +const { userModel } = require("@/modules/stores/mongo"); +const { login } = require("@/modules/auths/login"); -const { - registerDeviceToken, - unregisterDeviceToken, -} = require("../modules/fcm"); -const jwt = require("../modules/auths/jwt"); -const logger = require("../modules/logger"); +const { registerDeviceToken, unregisterDeviceToken } = require("@/modules/fcm"); +const jwt = require("@/modules/auths/jwt"); +const logger = require("@/modules/logger").default; -const { TOKEN_EXPIRED, TOKEN_INVALID } = require("../../loadenv").jwt; +const { TOKEN_EXPIRED, TOKEN_INVALID } = require("@/loadenv").jwt; const tokenLoginHandler = async (req, res) => { const { accessToken, deviceToken } = req.query; diff --git a/src/services/auth.replace.js b/src/services/auth.replace.js index 4103a7f0..111c7077 100644 --- a/src/services/auth.replace.js +++ b/src/services/auth.replace.js @@ -1,16 +1,16 @@ -const { userModel } = require("../modules/stores/mongo"); -const { logout, login } = require("../modules/auths/login"); +const { userModel } = require("@/modules/stores/mongo"); +const { logout, login } = require("@/modules/auths/login"); -const { unregisterDeviceToken } = require("../modules/fcm"); +const { unregisterDeviceToken } = require("@/modules/fcm"); const { generateNickname, generateProfileImageUrl, -} = require("../modules/modifyProfile"); -const logger = require("../modules/logger"); -const jwt = require("../modules/auths/jwt"); +} = require("@/modules/modifyProfile"); +const logger = require("@/modules/logger").default; +const jwt = require("@/modules/auths/jwt"); -const { registerDeviceTokenHandler, tryLogin } = require("../services/auth"); -const loginReplacePage = require("../views/loginReplacePage"); +const { registerDeviceTokenHandler, tryLogin } = require("@/services/auth"); +const loginReplacePage = require("@/views/loginReplacePage").default; const createUserData = (id) => { const info = { diff --git a/src/services/chats.js b/src/services/chats.js index c080411c..d9631c9b 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,12 +1,12 @@ -const { chatModel, userModel, roomModel } = require("../modules/stores/mongo"); -const { chatPopulateOption } = require("../modules/populates/chats"); -const aws = require("../modules/stores/aws"); +const { chatModel, userModel, roomModel } = require("@/modules/stores/mongo"); +const { chatPopulateOption } = require("@/modules/populates/chats"); +const aws = require("@/modules/stores/aws"); const { transformChatsForRoom, emitChatEvent, emitUpdateEvent, -} = require("../modules/socket"); -const logger = require("../modules/logger"); +} = require("@/modules/socket"); +const logger = require("@/modules/logger").default; const chatCount = 60; diff --git a/src/services/fare.js b/src/services/fare.js new file mode 100644 index 00000000..c8b41831 --- /dev/null +++ b/src/services/fare.js @@ -0,0 +1,98 @@ +const logger = require("@/modules/logger").default; + +const { naverMap } = require("@/loadenv"); +const { taxiFareModel, locationModel } = require("@/modules/stores/mongo"); +const { scaledTime, callTaxiFare } = require("@/modules/fare"); + +const naverMapApi = { + "X-NCP-APIGW-API-KEY-ID": naverMap.apiId, + "X-NCP-APIGW-API-KEY": naverMap.apiKey, +}; + +/** + * 주어진 from, to, time에 대한 택시 요금을 반환합니다. + * @summary 카이스트 본원 <-> 대전역의 경로를 제외한 다른 경로의 경우, 1주일 전 매일 18:00시의 택시 요금을 반환합니다. + * @summary 카이스트 본원 <-> 대전역의 경우, cron으로 1주일 전 미리 캐싱해놓은 데이터를 기반으로 주어진 시간에 대한 택시 요금을 반환합니다. 만일, 해당 데이터가 존재하지 않을 경우에는 직접 호출해 보여줍니다. + * @param {Request} req - 파라미터로 from, to, time을 받습니다. + * - @param {mongoose.Schema.Types.ObjectId} from - 출발지 + * - @param {mongoose.Schema.Types.ObjectId} to - 도착지 + * - @param {Date} time - 출발 시간 (ISO 8601) + */ +const getTaxiFareHandler = async (req, res) => { + try { + if ( + !naverMapApi["X-NCP-APIGW-API-KEY"] || + !naverMapApi["X-NCP-APIGW-API-KEY-ID"] + ) { + return res.status(503).json({ + error: "fare/getTaxiFareHandler: Naver Map API credential not found", + }); + } + + const from = await locationModel + .findOne({ + _id: { $eq: req.query.from }, + }) + .lean(); + const to = await locationModel + .findOne({ _id: { $eq: req.query.to } }) + .lean(); + const sTime = scaledTime(new Date(req.query.time)); + + if (!from || !to) { + return res + .status(400) + .json({ error: "fare/getTaxiFareHandler: Wrong location" }); + } else if (req.query.from === req.query.to) { + // 프론트엔드에서 예상 택시비를 숨기기 위해 0원을 반환 + return res.status(200).json({ fare: 0 }); + } + + const fare = await taxiFareModel + .findOne({ from: from._id, to: to._id, time: sTime }) + .lean(); + // 해당 sTime 대로 값이 존재하는 경우 (현재: 카이스트 본원 <-> 대전역) + if (fare) { + //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 + if (fare.fare <= 0) { + await callTaxiFare(from, to) + .then((fare) => { + res.status(200).json({ fare: fare }); + }) + .catch((err) => { + logger.error(err.message); + }); + } else { + res.status(200).json({ fare: fare.fare }); + } + } else { + const minorTaxiFare = await taxiFareModel + .findOne({ + from: from._id, + to: to._id, + time: 48 * new Date(req.query.time).getDay() + 0, + }) + .lean(); + + //만일 초기화 되지 않은 시간대의 정보를 필요로하는 비상시의 경우 대비 + if (!minorTaxiFare || minorTaxiFare.fare <= 0) { + await callTaxiFare(from, to) + .then((fare) => { + res.status(200).json({ fare: fare }); + }) + .catch((err) => { + logger.error(err.message); + }); + } else { + res.status(200).json({ fare: minorTaxiFare.fare }); + } + } + } catch (err) { + logger.error(err.message); + res + .status(500) + .json({ error: "fare/getTaxiFareHandler: Failed to load Taxi Fare" }); + } +}; + +module.exports = { getTaxiFareHandler }; diff --git a/src/services/locations.js b/src/services/locations.js index 5b02042d..9a2c5e7d 100644 --- a/src/services/locations.js +++ b/src/services/locations.js @@ -1,5 +1,5 @@ -const { locationModel } = require("../modules/stores/mongo"); -const logger = require("../modules/logger"); +const { locationModel } = require("@/modules/stores/mongo"); +const logger = require("@/modules/logger").default; const getAllLocationsHandler = async (_, res) => { try { diff --git a/src/services/logininfo.js b/src/services/logininfo.js index 0253a6c6..749f2e15 100644 --- a/src/services/logininfo.js +++ b/src/services/logininfo.js @@ -1,6 +1,6 @@ -const { userModel } = require("../modules/stores/mongo"); -const { getLoginInfo } = require("../modules/auths/login"); -const logger = require("../modules/logger"); +const { userModel } = require("@/modules/stores/mongo"); +const { getLoginInfo } = require("@/modules/auths/login"); +const logger = require("@/modules/logger").default; const logininfoHandler = async (req, res) => { try { diff --git a/src/services/notifications.js b/src/services/notifications.js index ac6753e9..f0c4bf94 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -1,11 +1,11 @@ -const { userModel } = require("../modules/stores/mongo"); -const { notificationOptionModel } = require("../modules/stores/mongo"); -const logger = require("../modules/logger"); +const { userModel } = require("@/modules/stores/mongo"); +const { notificationOptionModel } = require("@/modules/stores/mongo"); +const logger = require("@/modules/logger").default; -const { registerDeviceToken, validateDeviceToken } = require("../modules/fcm"); +const { registerDeviceToken, validateDeviceToken } = require("@/modules/fcm"); // 이벤트 코드입니다. -const { contracts } = require("../lottery"); +// const { contracts } = require("@/lottery"); const registerDeviceTokenHandler = async (req, res) => { try { @@ -111,11 +111,11 @@ const editOptionsHandler = async (req, res) => { } // 이벤트 코드입니다. - await contracts?.completeAdPushAgreementQuest( - req.userOid, - req.timestamp, - options.advertisement - ); + // await contracts?.completeAdPushAgreementQuest( + // req.userOid, + // req.timestamp, + // options.advertisement + // ); res.status(200).json(updatedNotificationOptions); } catch (err) { diff --git a/src/services/reports.js b/src/services/reports.js index 1ab77e90..d0d707c2 100644 --- a/src/services/reports.js +++ b/src/services/reports.js @@ -1,13 +1,9 @@ -const { - userModel, - reportModel, - roomModel, -} = require("../modules/stores/mongo"); -const { reportPopulateOption } = require("../modules/populates/reports"); -const { sendReportEmail } = require("../modules/email"); -const logger = require("../modules/logger"); -const reportEmailPage = require("../views/reportEmailPage"); -const { notifyReportToReportChannel } = require("../modules/slackNotification"); +const { userModel, reportModel, roomModel } = require("@/modules/stores/mongo"); +const { reportPopulateOption } = require("@/modules/populates/reports"); +const { sendReportEmail } = require("@/modules/email"); +const logger = require("@/modules/logger").default; +const reportEmailPage = require("@/views/reportEmailPage").default; +const { notifyReportToReportChannel } = require("@/modules/slackNotification"); const createHandler = async (req, res) => { try { diff --git a/src/services/rooms.js b/src/services/rooms.js index b121306a..ed93bcf8 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -2,25 +2,25 @@ const { roomModel, locationModel, userModel, -} = require("../modules/stores/mongo"); -const { emitChatEvent } = require("../modules/socket"); -const logger = require("../modules/logger"); +} = require("@/modules/stores/mongo"); +const { emitChatEvent } = require("@/modules/socket"); +const logger = require("@/modules/logger").default; const { roomPopulateOption, formatSettlement, getIsOver, -} = require("../modules/populates/rooms"); +} = require("@/modules/populates/rooms"); const { notifyRoomCreationAbuseToReportChannel, -} = require("../modules/slackNotification"); +} = require("@/modules/slackNotification"); // 이벤트 코드입니다. -const { eventConfig } = require("../../loadenv"); -const eventPeriod = eventConfig && { - startAt: new Date(eventConfig.period.startAt), - endAt: new Date(eventConfig.period.endAt), -}; -const { contracts } = require("../lottery"); +// const { eventConfig } = require("@/loadenv"); +// const eventPeriod = eventConfig && { +// startAt: new Date(eventConfig.period.startAt), +// endAt: new Date(eventConfig.period.endAt), +// }; +// const { contracts } = require("@/lottery"); const createHandler = async (req, res) => { const { name, from, to, time, maxPartLength } = req.body; @@ -110,7 +110,7 @@ const createHandler = async (req, res) => { const roomObjectFormated = formatSettlement(roomObject); // 이벤트 코드입니다. - await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp); + // await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp); return res.send(roomObjectFormated); } catch (err) { @@ -128,58 +128,59 @@ const createTestHandler = async (req, res) => { try { // 이벤트 코드입니다. - if ( - !eventPeriod || - req.timestamp >= eventPeriod.endAt || - req.timestamp < eventPeriod.startAt - ) - return res.json({ result: true }); - - const countRecentlyMadeRooms = await roomModel.countDocuments({ - madeat: { $gte: new Date(req.timestamp - 86400000) }, // 밀리초 단위로 24시간을 나타냅니다. - "part.0.user": req.userOid, // 방 최초 생성자를 저장하는 필드가 없으므로, 첫 번째 참여자를 생성자로 간주합니다. - }); - if (!countRecentlyMadeRooms && countRecentlyMadeRooms !== 0) - return res - .status(500) - .json({ error: "Rooms/create/test : internal server error" }); - - const dateTime = new Date(time); - const candidateRooms = await roomModel - .find( - { - time: { - $gte: new Date(dateTime.getTime() - 43200000), - $lte: new Date(dateTime.getTime() + 43200000), - }, - part: { $elemMatch: { user: req.userOid } }, - }, - "from to time maxPartLength" - ) - .limit(2) - .lean(); - if (!candidateRooms) - return res - .status(500) - .json({ error: "Rooms/create/test : internal server error" }); - - const isAbusing = checkIsAbusing( - req.body, - countRecentlyMadeRooms, - candidateRooms - ); - if (isAbusing) { - const user = await userModel - .findOne({ _id: req.userOid, withdraw: false }) - .lean(); - notifyRoomCreationAbuseToReportChannel( - req.userOid, - user?.nickname ?? req.userOid, - req.body - ); - } + // if ( + // !eventPeriod || + // req.timestamp >= eventPeriod.endAt || + // req.timestamp < eventPeriod.startAt + // ) + // return res.json({ result: true }); + + // const countRecentlyMadeRooms = await roomModel.countDocuments({ + // madeat: { $gte: new Date(req.timestamp - 86400000) }, // 밀리초 단위로 24시간을 나타냅니다. + // "part.0.user": req.userOid, // 방 최초 생성자를 저장하는 필드가 없으므로, 첫 번째 참여자를 생성자로 간주합니다. + // }); + // if (!countRecentlyMadeRooms && countRecentlyMadeRooms !== 0) + // return res + // .status(500) + // .json({ error: "Rooms/create/test : internal server error" }); + + // const dateTime = new Date(time); + // const candidateRooms = await roomModel + // .find( + // { + // time: { + // $gte: new Date(dateTime.getTime() - 43200000), + // $lte: new Date(dateTime.getTime() + 43200000), + // }, + // part: { $elemMatch: { user: req.userOid } }, + // }, + // "from to time maxPartLength" + // ) + // .limit(2) + // .lean(); + // if (!candidateRooms) + // return res + // .status(500) + // .json({ error: "Rooms/create/test : internal server error" }); + + // const isAbusing = checkIsAbusing( + // req.body, + // countRecentlyMadeRooms, + // candidateRooms + // ); + // if (isAbusing) { + // const user = await userModel + // .findOne({ _id: req.userOid, withdraw: false }) + // .lean(); + // notifyRoomCreationAbuseToReportChannel( + // req.userOid, + // user?.nickname ?? req.userOid, + // req.body + // ); + // } - return res.json({ result: !isAbusing }); + // return res.json({ result: !isAbusing }); + return res.json({ result: true }); } catch (err) { logger.error(err); res.status(500).json({ @@ -588,16 +589,11 @@ const commitSettlementHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completePayingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); + // await contracts?.completeFareSettlementQuest( + // req.userOid, + // req.timestamp, + // roomObject + // ); // 수정한 방 정보를 반환합니다. res.send(formatSettlement(roomObject, { isOver: true })); @@ -666,16 +662,11 @@ const commitPaymentHandler = async (req, res) => { }); // 이벤트 코드입니다. - await contracts?.completeSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); - await contracts?.completePayingAndSendingQuest( - req.userOid, - req.timestamp, - roomObject - ); + // await contracts?.completeFarePaymentQuest( + // req.userOid, + // req.timestamp, + // roomObject + // ); // 수정한 방 정보를 반환합니다. res.send(formatSettlement(roomObject, { isOver: true })); diff --git a/src/services/users.js b/src/services/users.ts similarity index 55% rename from src/services/users.js rename to src/services/users.ts index 2c73d1cf..bd95a06e 100644 --- a/src/services/users.js +++ b/src/services/users.ts @@ -1,51 +1,70 @@ -const { userModel, banModel } = require("../modules/stores/mongo"); -const { unregisterAllDeviceTokens } = require("../modules/fcm"); -const logger = require("../modules/logger"); -const aws = require("../modules/stores/aws"); - -// 이벤트 코드입니다. -const { contracts } = require("../lottery"); -const { +import type { RequestHandler } from "express"; +import { unregisterAllDeviceTokens } from "@/modules/fcm"; +import logger from "@/modules/logger"; +import { generateNickname, generateProfileImageUrl, -} = require("../modules/modifyProfile"); +} from "@/modules/modifyProfile"; +import * as aws from "@/modules/stores/aws"; +import { userModel, banModel } from "@/modules/stores/mongo"; -const agreeOnTermsOfServiceHandler = async (req, res) => { +// 이벤트 코드입니다. +// const { contracts } = require("@/lottery"); + +export const agreeOnTermsOfServiceHandler: RequestHandler = async ( + req, + res +) => { try { let user = await userModel.findOne({ _id: req.userOid, withdraw: false }); - if (user.agreeOnTermsOfService !== true) { - user.agreeOnTermsOfService = true; - await user.save(); - res - .status(200) - .send( - "Users/agreeOnTermsOfService : agree on Terms of Service successful" - ); - } else { - res.status(400).send("Users/agreeOnTermsOfService : already agreed"); + if (!user) { + return res.status(400).send("Users/agreeOnTermsOfService : no such user"); + } + + if (user.agreeOnTermsOfService === true) { + return res + .status(400) + .send("Users/agreeOnTermsOfService : already agreed"); } + + user.agreeOnTermsOfService = true; + await user.save(); + return res + .status(200) + .send( + "Users/agreeOnTermsOfService : agree on Terms of Service successful" + ); } catch { - res.status(500).send("Users/agreeOnTermsOfService : internal server error"); + return res + .status(500) + .send("Users/agreeOnTermsOfService : internal server error"); } }; -const getAgreeOnTermsOfServiceHandler = async (req, res) => { +export const getAgreeOnTermsOfServiceHandler: RequestHandler = async ( + req, + res +) => { try { const user = await userModel .findOne({ _id: req.userOid, withdraw: false }, "agreeOnTermsOfService") .lean(); + if (!user) { + return res.status(400).send("Users/agreeOnTermsOfService : no such user"); + } + const agreeOnTermsOfService = user.agreeOnTermsOfService === true; - res.json({ agreeOnTermsOfService }); + return res.json({ agreeOnTermsOfService }); } catch { - res + return res .status(500) .send("Users/getAgreeOnTermsOfService : internal server error"); } }; -const editNicknameHandler = async (req, res) => { +export const editNicknameHandler: RequestHandler = async (req, res) => { try { - const newNickname = req.body.nickname; + const newNickname = req.body.nickname; // TODO: Typing const result = await userModel.findOneAndUpdate( { _id: req.userOid, withdraw: false }, { nickname: newNickname } @@ -53,26 +72,28 @@ const editNicknameHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. - await contracts?.completeNicknameChangingQuest( - req.userOid, - req.timestamp - ); + // await contracts?.completeNicknameChangingQuest( + // req.userOid, + // req.timestamp + // ); - res + return res .status(200) .send("Users/editNickname : edit user nickname successful"); } else { - res.status(400).send("Users/editNickname : such user id does not exist"); + return res + .status(400) + .send("Users/editNickname : such user id does not exist"); } } catch (err) { logger.error(err); - res.status(500).send("Users/editNickname : internal server error"); + return res.status(500).send("Users/editNickname : internal server error"); } }; -const editAccountHandler = async (req, res) => { +export const editAccountHandler: RequestHandler = async (req, res) => { try { - const newAccount = req.body.account; + const newAccount = req.body.account; // TODO: Typing const result = await userModel.findOneAndUpdate( { _id: req.userOid, withdraw: false }, { account: newAccount } @@ -80,25 +101,32 @@ const editAccountHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. - await contracts?.completeAccountChangingQuest( - req.userOid, - req.timestamp, - newAccount - ); + // await contracts?.completeAccountChangingQuest( + // req.userOid, + // req.timestamp, + // newAccount + // ); - res.status(200).send("Users/editAccount : edit user account successful"); + return res + .status(200) + .send("Users/editAccount : edit user account successful"); } else { - res.status(400).send("Users/editAccount : such user id does not exist"); + return res + .status(400) + .send("Users/editAccount : such user id does not exist"); } } catch (err) { logger.error(err); - res.status(500).send("Users/editAccount : internal server error"); + return res.status(500).send("Users/editAccount : internal server error"); } }; -const editProfileImgGetPUrlHandler = async (req, res) => { +export const editProfileImgGetPUrlHandler: RequestHandler = async ( + req, + res +) => { try { - const type = req.body.type; + const type = req.body.type; // TODO: Typing const user = await userModel.findOne( { _id: req.userOid, withdraw: false }, "_id" @@ -117,19 +145,19 @@ const editProfileImgGetPUrlHandler = async (req, res) => { } data.fields["Content-Type"] = type; data.fields["key"] = key; - res.json({ + return res.json({ url: data.url, fields: data.fields, }); }); } catch (e) { - res + return res .status(500) .send("Users/editProfileImg/getPUrl : internal server error"); } }; -const editProfileImgDoneHandler = async (req, res) => { +export const editProfileImgDoneHandler: RequestHandler = async (req, res) => { try { const user = await userModel.findOne( { _id: req.userOid, withdraw: false }, @@ -158,37 +186,39 @@ const editProfileImgDoneHandler = async (req, res) => { .status(500) .send("Users/editProfileImg/done : internal server error"); } - res.json({ + return res.json({ result: true, profileImageUrl: userAfter.profileImageUrl, }); }); } catch (e) { - res.status(500).send("Users/editProfileImg/done : internal server error"); + return res + .status(500) + .send("Users/editProfileImg/done : internal server error"); } }; -const resetNicknameHandler = async (req, res) => { +export const resetNicknameHandler: RequestHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( { _id: req.userOid, withdraw: false }, - { nickname: generateNickname(req.body.id) }, + { nickname: generateNickname(req.body.id) }, // TODO: Typing or Validation { new: true } ); if (!result) return res .status(400) .send("Users/resetNickname : such user does not exist"); - res + return res .status(200) .send("Users/resetNickname : reset user nickname successful"); } catch (err) { logger.error(err); - res.status(500).send("Users/resetNickname : internal server error"); + return res.status(500).send("Users/resetNickname : internal server error"); } }; -const resetProfileImgHandler = async (req, res) => { +export const resetProfileImgHandler: RequestHandler = async (req, res) => { try { const result = await userModel.findOneAndUpdate( { _id: req.userOid, withdraw: false }, @@ -199,44 +229,33 @@ const resetProfileImgHandler = async (req, res) => { return res .status(400) .send("Users/resetProfileImg : such user does not exist"); - res + return res .status(200) .send("Users/resetProfileImg : reset user profile image successful"); } catch (err) { - res.status(500).send("Users/resetProfileImg : internal server error"); - } -}; - -const isBannedHandler = async (req, res) => { - try { - // 현재 시각이 expireAt 보다 작고 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 - const result = await banModel.find({ - userId: req.userOid, - expireAt: { - $gte: req.timestamp, - }, - }); - if (!result) - return res.status(500).send("Users/isBanned : internal server error"); - res.status(200).json(result); - } catch (err) { - res.status(500).send("Users/isBanned : internal server error"); + return res + .status(500) + .send("Users/resetProfileImg : internal server error"); } }; -const getBanRecordHandler = async (req, res) => { +export const getBanRecordHandler: RequestHandler = async (req, res) => { try { - // 본인인 경우(ban의 userId가 userOid랑 같은 경우)의 record를 모두 가져옴 - const result = await banModel.find({ userId: req.userOid }); + // 본인인 경우(ban의 userId가 userSid랑 같은 경우)의 record를 모두 가져옴 + const result = await banModel + .find({ + userSid: req.session.loginInfo!.sid, + }) + .sort({ expireAt: -1 }); if (!result) return res.status(500).send("Users/getBanRecord : internal server error"); - res.status(200).json(result); + return res.status(200).json(result); } catch (err) { - res.status(500).send("Users/getBanRecord : internal server error"); + return res.status(500).send("Users/getBanRecord : internal server error"); } }; -const withdrawHandler = async (req, res) => { +export const withdrawHandler: RequestHandler = async (req, res) => { try { const user = await userModel.findOne({ _id: req.userOid, withdraw: false }); if (!user) { @@ -246,16 +265,16 @@ const withdrawHandler = async (req, res) => { // 회원 탈퇴가 가능한 조건인지 확인 if (user.withdraw) { return res.status(400).send("Users/withdraw : already withdrawn"); - } else if (user.ongoingRoom.length !== 0) { + } else if (user.ongoingRoom?.length !== 0) { return res.status(400).send("Users/withdraw : ongoing room exists"); } // 등록된 모든 디바이스 토큰 삭제 - await unregisterAllDeviceTokens(req.userOid); + await unregisterAllDeviceTokens(req.userOid!); // 회원 탈퇴 처리 (Soft Delete) user.withdraw = true; - user.withdrawAt = req.timestamp; + user.withdrawAt = new Date(req.timestamp!); await user.save(); @@ -264,17 +283,3 @@ const withdrawHandler = async (req, res) => { res.status(500).send("Users/withdraw : internal server error"); } }; - -module.exports = { - agreeOnTermsOfServiceHandler, - getAgreeOnTermsOfServiceHandler, - editNicknameHandler, - editAccountHandler, - editProfileImgGetPUrlHandler, - editProfileImgDoneHandler, - resetNicknameHandler, - resetProfileImgHandler, - isBannedHandler, - getBanRecordHandler, - withdrawHandler, -}; diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 00000000..f8a54993 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,17 @@ +// to make the file a module and avoid the TypeScript error +export {}; + +declare global { + namespace Express { + export interface Request { + /** 사용자의 ObjectID. MongoDB에서 사용됩니다. */ + userOid?: string; + /** 요청의 origin. */ + origin?: string; + /** 사용자의 IP 주소. */ + clientIP?: string; + /** 요청의 timestamp. */ + timestamp?: number; + } + } +} diff --git a/src/types/mongo.d.ts b/src/types/mongo.d.ts new file mode 100644 index 00000000..39b66199 --- /dev/null +++ b/src/types/mongo.d.ts @@ -0,0 +1,195 @@ +import type { Document, Types } from "mongoose"; + +export interface User extends Document { + /** 사용자의 실명. */ + name: string; + /** 사용자의 닉네임. */ + nickname: string; + /** Taxi에서만 사용되는 사용자의 ID. */ + id: string; + /** 계정 프로필 이미지 주소. */ + profileImageUrl: string; + /** 사용자가 참여한 방 중 현재 진행 중인 방의 배열. */ + ongoingRoom?: Types.Array; + /** 사용자가 참여한 방 중 완료된 방의 배열. */ + doneRoom?: Types.Array; + /** 계정 탈퇴 여부. */ + withdraw: boolean; + /** 계정 탈퇴 시각. */ + withdrawAt?: Date; + /** 사용자의 전화번호. 2023 가을 이벤트부터 추가됨. */ + phoneNumber?: string; + /** 계정 정지 여부. */ + ban: boolean; + /** 계정 가입 시각. */ + joinat: Date; + /** 사용자의 Taxi 이용약관 동의 여부. */ + agreeOnTermsOfService: boolean; + subinfo?: { + /** 사용자의 KAIST 학번. */ + kaist: string; + sparcs: string; + facebook: string; + twitter: string; + }; + /** 사용자의 이메일 주소. */ + email: string; + /** 계정의 관리자 여부. */ + isAdmin: boolean; + /** 사용자의 계좌번호 정보. */ + account: string; +} + +export interface Ban extends Document { + /** 정지된 사용자의 ID. */ + userSid: string; + /** 정지 사유. */ + reason: string; + /** 정지 시각. */ + bannedAt: Date; + /** 정지 만료 시각. */ + expireAt: Date; + /** 정지된 서비스의 이름. */ + serviceName: "service" | "2023-fall-event"; +} + +export type SettlementStatus = + | "not-departed" + | "paid" + | "send-required" + | "sent"; + +export interface Participant extends Document { + /** 방 참여자의 User ObjectID. */ + user: Types.ObjectId; + /** 방 참여자의 정산 상태. */ + settlementStatus: SettlementStatus; + /** 방 참여자가 마지막으로 채팅을 읽은 시각. */ + readAt?: Date; +} + +export interface DeviceToken extends Document { + /** 디바이스 토큰 소유자의 User ObjectID. */ + userId: Types.ObjectId; + /** 소유한 디바이스 토큰의 배열. */ + deviceTokens: Types.Array; +} + +export interface NotificationOption extends Document { + deviceToken: string; + /** 채팅 알림 수신 여부. */ + chatting: boolean; + /** 방 알림 키워드. */ + keywords: Types.Array; + /** 출발 전 알림 발송 여부. */ + beforeDepart: boolean; + /** 공지성 알림 수신 여부. */ + notice: boolean; + /** 광고성 알림 수신 여부. */ + advertisement: boolean; +} + +export interface TopicSubscription extends Document { + deviceToken?: string; + topic?: string; + subscribedAt: Date; +} + +export interface Room extends Document { + /** 방의 이름. */ + name: string; + /** 방의 출발지의 Location ObjectID. */ + from: Types.ObjectId; + /** 방의 목적지의 Location ObjectID. */ + to: Types.ObjectId; + /** 방의 출발 시각. */ + time: Date; + /** 방 참여자의 배열. */ + part?: Types.DocumentArray; + /** 방의 생성 시각. */ + madeat: Date; + /** 방 참여자 중 정산을 완료한 참여자의 수. */ + settlementTotal: number; + /** 방의 최대 참여자 수. */ + maxPartLength: number; +} + +export interface Location extends Document { + enName: string; + koName: string; + priority: number; + isValid: boolean; + /** 위도. */ + latitude: number; + /** 경도. */ + longitude: number; +} + +export type ChatType = + | "text" + | "in" + | "out" + | "s3img" + | "payment" + | "settlement" + | "account" + | "departure" + | "arrival"; + +export interface Chat extends Document { + /** 메세지가 전송된 방의 Room ObjectID. */ + roomId: Types.ObjectId; + /** 메세지의 종류. */ + type?: ChatType; + /** 메세지의 작성자의 User ObjectID. */ + authorId?: Types.ObjectId; + content: string; + time: Date; + isValid: boolean; +} + +export interface Report extends Document { + /** 신고한 사용자의 ObjectID. */ + creatorId: Types.ObjectId; + /** 신고받은 사용자의 ObjectID. */ + reportedId: Types.ObjectId; + /** 신고의 종류. */ + type: "no-settlement" | "no-show" | "etc-reason"; + /** 신고의 기타 세부 사유. */ + etcDetail: string; + /** 신고한 시각. */ + time: Date; + /** 신고한 방의 ObjectID. */ + roomId?: Types.ObjectId; +} + +export interface AdminIPWhitelist extends Document { + ip: string; + description: string; +} + +export type AdminLogAction = "create" | "read" | "update" | "delete"; + +export interface AdminLog extends Document { + /** 로그 발생자의 User ObjectID. */ + user: Types.ObjectId; + /** 로그의 발생 시각. */ + time: Date; + /** 로그의 발생 IP 주소. */ + ip: string; + /** 취급한 대상. */ + target: string; + /** 수행한 업무. */ + action: AdminLogAction; +} + +export interface TaxiFare extends Document { + /** 출발지의 Location ObjectID. */ + from: Types.ObjectId; + /** 목적지의 Location ObjectID. */ + to: Types.ObjectId; + isMajor: boolean; + time: number; + /** 예상 택시 요금. */ + fare: number; +} diff --git a/src/views/emailPage.js b/src/views/emailPage.ts similarity index 92% rename from src/views/emailPage.js rename to src/views/emailPage.ts index f41122a4..c876e0d5 100644 --- a/src/views/emailPage.js +++ b/src/views/emailPage.ts @@ -1,8 +1,8 @@ -const { getS3Url } = require("../modules/stores/aws"); +import { getS3Url } from "@/modules/stores/aws"; -module.exports = ( - title, - content +const emailPage = ( + title: string, + content: string ) => ` @@ -29,3 +29,5 @@ module.exports = ( `; + +export default emailPage; diff --git a/src/views/loginReplacePage.js b/src/views/loginReplacePage.ts similarity index 97% rename from src/views/loginReplacePage.js rename to src/views/loginReplacePage.ts index 972cfb62..5a91cfca 100644 --- a/src/views/loginReplacePage.js +++ b/src/views/loginReplacePage.ts @@ -1,4 +1,4 @@ -module.exports = ` +const loginPage = ` @@ -44,3 +44,5 @@ module.exports = ` `; + +export default loginPage; diff --git a/src/views/reportEmailPage.js b/src/views/reportEmailPage.ts similarity index 91% rename from src/views/reportEmailPage.js rename to src/views/reportEmailPage.ts index 4b583e9c..49691aad 100644 --- a/src/views/reportEmailPage.js +++ b/src/views/reportEmailPage.ts @@ -1,6 +1,18 @@ -const emailPage = require("./emailPage"); +import type { ObjectId } from "mongoose"; +import emailPage from "./emailPage"; -const reportEmailPage = {}; +interface ReportEmailPage { + [key: string]: ( + origin: string, + name: string, + nickname: string, + roomName: string, + payer: string, + roomId: string | ObjectId + ) => string; +} + +const reportEmailPage: ReportEmailPage = {}; /* 미정산 알림 메일을 위한 템플릿 */ reportEmailPage["no-settlement"] = ( @@ -83,4 +95,4 @@ reportEmailPage["no-show"] = ( ` ); -module.exports = reportEmailPage; +export default reportEmailPage; diff --git a/test/utils.js b/test/utils.js index e537913b..ba8d2bca 100644 --- a/test/utils.js +++ b/test/utils.js @@ -7,7 +7,7 @@ const { connectDatabase, } = require("../src/modules/stores/mongo"); const { generateProfileImageUrl } = require("../src/modules/modifyProfile"); -const { mongo: mongoUrl } = require("../loadenv"); +const { mongo: mongoUrl } = require("@/loadenv"); connectDatabase(mongoUrl); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 00000000..68db80dd --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..166f2de6 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "node16", + "moduleResolution": "node16", + "allowJs": true, + "outDir": "./dist", + "resolveJsonModule": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": "./src", + "paths": { + "@/*": ["./*"] + } + }, + "include": ["src", "scripts"], + "exclude": ["dist", "node_modules"], + "ts-node": { + "files": true + } +}