diff --git a/.gitignore b/.gitignore index 8ffa241b..a7e86767 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.code-workspace *.swp /logs/*.log +.vscode # AdminJS 관련 디렉토리 .adminjs diff --git a/app.js b/app.js index db40affb..d8a1f0f3 100644 --- a/app.js +++ b/app.js @@ -1,8 +1,9 @@ // 모듈 require const express = require("express"); const http = require("http"); -const { port: httpPort } = require("./loadenv"); +const { port: httpPort, eventMode } = require("./loadenv"); const logger = require("./src/modules/logger"); +const { connectDatabase } = require("./src/modules/stores/mongo"); const { startSocketServer } = require("./src/modules/socket"); // Firebase Admin 초기설정 @@ -11,6 +12,9 @@ require("./src/modules/fcm").initializeApp(); // 익스프레스 서버 생성 const app = express(); +// 데이터베이스 연결 +connectDatabase(); + // [Middleware] request body 파싱 app.use(express.urlencoded({ extended: false })); app.use(express.json()); @@ -38,6 +42,10 @@ app.use(require("./src/middlewares/limitRate")); // [Router] Swagger (API 문서) app.use("/docs", require("./src/routes/docs")); +// 2023 추석 이벤트 전용 라우터입니다. +eventMode && + app.use(`/events/${eventMode}`, require("./src/lottery").lotteryRouter); + // [Middleware] 모든 API 요청에 대하여 origin 검증 app.use(require("./src/middlewares/originValidator")); @@ -61,5 +69,8 @@ const serverHttp = http logger.info(`Express 서버가 ${httpPort}번 포트에서 시작됨.`) ); -// socket.io 서버 시작 및 app 인스턴스에 저장 +// socket.io 서버 시작 app.set("io", startSocketServer(serverHttp)); + +// [Schedule] 스케줄러 시작 +require("./src/schedules")(app); diff --git a/loadenv.js b/loadenv.js index 62c50d7f..2bce4fb9 100644 --- a/loadenv.js +++ b/loadenv.js @@ -38,4 +38,5 @@ module.exports = { slackWebhookUrl: { report: process.env.SLACK_REPORT_WEBHOOK_URL || "", // optional }, + eventMode: undefined, }; diff --git a/package.json b/package.json index cca22460..cdb57f4f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "firebase-admin": "^11.4.1", "jsonwebtoken": "^8.5.1", "mongoose": "^6.11.3", + "node-cron": "3.0.2", "node-mocks-http": "^1.12.1", "querystring": "^0.2.1", "redis": "^4.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bf55d4e..2249f8ac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ dependencies: mongoose: specifier: ^6.11.3 version: 6.11.5 + node-cron: + specifier: 3.0.2 + version: 3.0.2 node-mocks-http: specifier: ^1.12.1 version: 1.12.2 @@ -217,6 +220,7 @@ packages: /@aws-crypto/crc32@3.0.0: resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + requiresBuild: true dependencies: '@aws-crypto/util': 3.0.0 '@aws-sdk/types': 3.378.0 @@ -226,6 +230,7 @@ packages: /@aws-crypto/ie11-detection@3.0.0: resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + requiresBuild: true dependencies: tslib: 1.14.1 dev: false @@ -233,6 +238,7 @@ packages: /@aws-crypto/sha256-browser@3.0.0: resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + requiresBuild: true dependencies: '@aws-crypto/ie11-detection': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -247,6 +253,7 @@ packages: /@aws-crypto/sha256-js@3.0.0: resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + requiresBuild: true dependencies: '@aws-crypto/util': 3.0.0 '@aws-sdk/types': 3.378.0 @@ -256,6 +263,7 @@ packages: /@aws-crypto/supports-web-crypto@3.0.0: resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + requiresBuild: true dependencies: tslib: 1.14.1 dev: false @@ -263,6 +271,7 @@ packages: /@aws-crypto/util@3.0.0: resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@aws-sdk/util-utf8-browser': 3.259.0 @@ -273,6 +282,7 @@ packages: /@aws-sdk/client-cognito-identity@3.385.0: resolution: {integrity: sha512-fRXZhxvBBeK/Jxb+sLPhyQmcduNSugSKJDz474A/wLK5UIuDOnKhDTjsa0OXMpY5DkqwdYLwDcGZtxUbEZ8DCQ==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -318,6 +328,7 @@ packages: /@aws-sdk/client-sso@3.382.0: resolution: {integrity: sha512-ge11t4hJllOF8pBNF0p1X52lLqUsLGAoey24fvk3fyvvczeLpegGYh2kdLG0iwFTDgRxaUqK+kboH5Wy9ux/pw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -360,6 +371,7 @@ packages: /@aws-sdk/client-sts@3.385.0: resolution: {integrity: sha512-VdSDwICW2cBttbdj1izu6VYflJbZZKu3/FSaJGuGu8SgTvRsa56g6E5xfbUfR/SCstuETObKLusSfQZ6yxUnzA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 @@ -406,6 +418,7 @@ packages: /@aws-sdk/credential-provider-cognito-identity@3.385.0: resolution: {integrity: sha512-NeWJgI2XdfO0ZM25KsfNx9CDmLByY3ymVc0ae4Os+bd8pJsFeo1rX3NSkyw8XGryEbOlVJ3Jz5W5huhjo4LvqQ==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/client-cognito-identity': 3.385.0 '@aws-sdk/types': 3.378.0 @@ -420,6 +433,7 @@ packages: /@aws-sdk/credential-provider-env@3.378.0: resolution: {integrity: sha512-B2OVdO9kBClDwGgWTBLAQwFV8qYTYGyVujg++1FZFSFMt8ORFdZ5fNpErvJtiSjYiOOQMzyBeSNhKyYNXCiJjQ==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/property-provider': 2.0.1 @@ -431,6 +445,7 @@ packages: /@aws-sdk/credential-provider-ini@3.385.0: resolution: {integrity: sha512-WBIR5GdfUzCGzynQYX/TuCXw3KJCkHBk6bVAsO1YmfR68XKVAxWmJPKovlK/rR6LIuV+iwUMNludO+SkmG0efg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/credential-provider-env': 3.378.0 '@aws-sdk/credential-provider-process': 3.378.0 @@ -450,6 +465,7 @@ packages: /@aws-sdk/credential-provider-node@3.385.0: resolution: {integrity: sha512-Lk8uu6jm/8OkbLX4Qnss8o5bnt0yQa0Tb7Azbh5/5otju5kStVAD2E+zMGrMP++NriGyZV87crduh0J8l4JUTA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/credential-provider-env': 3.378.0 '@aws-sdk/credential-provider-ini': 3.385.0 @@ -470,6 +486,7 @@ packages: /@aws-sdk/credential-provider-process@3.378.0: resolution: {integrity: sha512-KFTIy7u+wXj3eDua4rgS0tODzMnXtXhAm1RxzCW9FL5JLBBrd82ymCj1Dp72217Sw5Do6NjCnDTTNkCHZMA77w==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/property-provider': 2.0.1 @@ -482,6 +499,7 @@ packages: /@aws-sdk/credential-provider-sso@3.385.0: resolution: {integrity: sha512-ETFnS+4ZKTAgT8boVpIpRuXA9wWGpNqOcI1RXtjsaIgQ9s8uNn2JPa8l71gZh861mzBC8Hadp1EpNu+43w4lkg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/client-sso': 3.382.0 '@aws-sdk/token-providers': 3.385.0 @@ -498,6 +516,7 @@ packages: /@aws-sdk/credential-provider-web-identity@3.378.0: resolution: {integrity: sha512-GWjydOszhc4xDF8xuPtBvboglXQr0gwCW1oHAvmLcOT38+Hd6qnKywnMSeoXYRPgoKfF9TkWQgW1jxplzCG0UA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/property-provider': 2.0.1 @@ -534,6 +553,7 @@ packages: /@aws-sdk/middleware-host-header@3.379.1: resolution: {integrity: sha512-LI4KpAFWNWVr2aH2vRVblr0Y8tvDz23lj8LOmbDmCrzd5M21nxuocI/8nEAQj55LiTIf9Zs+dHCdsyegnFXdrA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/protocol-http': 2.0.1 @@ -545,6 +565,7 @@ packages: /@aws-sdk/middleware-logger@3.378.0: resolution: {integrity: sha512-l1DyaDLm3KeBMNMuANI3scWh8Xvu248x+vw6Z7ExWOhGXFmQ1MW7YvASg/SdxWkhlF9HmkkTif1LdMB22x6QDA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/types': 2.0.2 @@ -555,6 +576,7 @@ packages: /@aws-sdk/middleware-recursion-detection@3.378.0: resolution: {integrity: sha512-mUMfHAz0oGNIWiTZHTVJb+I515Hqs2zx1j36Le4MMiiaMkPW1SRUF1FIwGuc1wh6E8jB5q+XfEMriDjRi4TZRA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/protocol-http': 2.0.1 @@ -566,6 +588,7 @@ packages: /@aws-sdk/middleware-sdk-sts@3.379.1: resolution: {integrity: sha512-SK3gSyT0XbLiY12+AjLFYL9YngxOXHnZF3Z33Cdd4a+AUYrVBV7JBEEGD1Nlwrcmko+3XgaKlmgUaR5s91MYvg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/middleware-signing': 3.379.1 '@aws-sdk/types': 3.378.0 @@ -577,6 +600,7 @@ packages: /@aws-sdk/middleware-signing@3.379.1: resolution: {integrity: sha512-kBk2ZUvR84EM4fICjr8K+Ykpf8SI1UzzPp2/UVYZ0X+4H/ZCjfSqohGRwHykMqeplne9qHSL7/rGJs1H3l3gPg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/property-provider': 2.0.1 @@ -591,6 +615,7 @@ packages: /@aws-sdk/middleware-user-agent@3.382.0: resolution: {integrity: sha512-LFRW1jmXOrOAd3911ktn6oaYmuurNnulbdRMOUdwz99GGdLVFipQhOi9idKswb8IOhPa4jEVQt25Kcv7ctvu0A==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@aws-sdk/util-endpoints': 3.382.0 @@ -603,6 +628,7 @@ packages: /@aws-sdk/token-providers@3.385.0: resolution: {integrity: sha512-2A2Y7/bU5EaxQwLwLy7ojs+Wy5VOBkIlGPH7ZcpPaoQ1Hscwn3Wvx/DZmOvbyYfZ1CbIFutoHJlVxh6KZldUDw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/property-provider': 2.0.1 @@ -615,6 +641,7 @@ packages: /@aws-sdk/types@3.378.0: resolution: {integrity: sha512-qP0CvR/ItgktmN8YXpGQglzzR/6s0nrsQ4zIfx3HMwpsBTwuouYahcCtF1Vr82P4NFcoDA412EJahJ2pIqEd+w==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -624,6 +651,7 @@ packages: /@aws-sdk/util-endpoints@3.382.0: resolution: {integrity: sha512-flajPyjmjNG67fXk7l4GoTB/7J11VBqtFZXuuAZKhKU07Ia3IQupsFqNf5lV8D44ZgjnKH0fTGnv3dUALjW7Wg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 tslib: 2.6.1 @@ -633,6 +661,7 @@ packages: /@aws-sdk/util-locate-window@3.310.0: resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -640,6 +669,7 @@ packages: /@aws-sdk/util-user-agent-browser@3.378.0: resolution: {integrity: sha512-FSCpagzftK1W+m7Ar6lpX7/Gr9y5P56nhFYz8U4EYQ4PkufS6czWX9YW+/FA5OYV0vlQ/SvPqMnzoHIPUNhZrQ==} + requiresBuild: true dependencies: '@aws-sdk/types': 3.378.0 '@smithy/types': 2.0.2 @@ -651,6 +681,7 @@ packages: /@aws-sdk/util-user-agent-node@3.378.0: resolution: {integrity: sha512-IdwVJV0E96MkJeFte4dlWqvB+oiqCiZ5lOlheY3W9NynTuuX0GGYNC8Y9yIsV8Oava1+ujpJq0ww6qXdYxmO4A==} engines: {node: '>=14.0.0'} + requiresBuild: true peerDependencies: aws-crt: '>=1.0.0' peerDependenciesMeta: @@ -666,6 +697,7 @@ packages: /@aws-sdk/util-utf8-browser@3.259.0: resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2255,6 +2287,7 @@ packages: /@google-cloud/paginator@3.0.7: resolution: {integrity: sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ==} engines: {node: '>=10'} + requiresBuild: true dependencies: arrify: 2.0.1 extend: 3.0.2 @@ -2264,12 +2297,14 @@ packages: /@google-cloud/projectify@3.0.0: resolution: {integrity: sha512-HRkZsNmjScY6Li8/kb70wjGlDDyLkVk3KvoEo9uIoxSjYLJasGiCch9+PqRVDOCGUFvEIqyogl+BeqILL4OJHA==} engines: {node: '>=12.0.0'} + requiresBuild: true dev: false optional: true /@google-cloud/promisify@3.0.1: resolution: {integrity: sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==} engines: {node: '>=12'} + requiresBuild: true dev: false optional: true @@ -2305,6 +2340,7 @@ packages: /@grpc/grpc-js@1.8.21: resolution: {integrity: sha512-KeyQeZpxeEBSqFVTi3q2K7PiPXmgBfECc4updA1ejCLjYmoAlvvM3ZMp5ztTDUCUQmoY3CpDxvchjO1+rFkoHg==} engines: {node: ^8.13.0 || >=10.10.0} + requiresBuild: true dependencies: '@grpc/proto-loader': 0.7.8 '@types/node': 20.4.7 @@ -2315,6 +2351,7 @@ packages: resolution: {integrity: sha512-GU12e2c8dmdXb7XUlOgYWZ2o2i+z9/VeACkxTA/zzAe2IjclC5PnVL0lpgjhrqfpDYHzM8B1TF6pqWegMYAzlA==} engines: {node: '>=6'} hasBin: true + requiresBuild: true dependencies: '@types/long': 4.0.2 lodash.camelcase: 4.3.0 @@ -2417,6 +2454,7 @@ packages: /@jsdoc/salty@0.2.5: resolution: {integrity: sha512-TfRP53RqunNe2HBobVBJ0VLhK1HbfvBYeTC1ahnN64PWvyYyGebmMiPkuwvD9fpw2ZbkoPb8Q7mwy0aR8Z9rvw==} engines: {node: '>=v12.0.0'} + requiresBuild: true dependencies: lodash: 4.17.21 dev: false @@ -2446,26 +2484,31 @@ packages: /@protobufjs/aspromise@1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + requiresBuild: true dev: false optional: true /@protobufjs/base64@1.1.2: resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + requiresBuild: true dev: false optional: true /@protobufjs/codegen@2.0.4: resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + requiresBuild: true dev: false optional: true /@protobufjs/eventemitter@1.1.0: resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + requiresBuild: true dev: false optional: true /@protobufjs/fetch@1.1.0: resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + requiresBuild: true dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/inquire': 1.1.0 @@ -2474,26 +2517,31 @@ packages: /@protobufjs/float@1.0.2: resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + requiresBuild: true dev: false optional: true /@protobufjs/inquire@1.1.0: resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + requiresBuild: true dev: false optional: true /@protobufjs/path@1.1.2: resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + requiresBuild: true dev: false optional: true /@protobufjs/pool@1.1.0: resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + requiresBuild: true dev: false optional: true /@protobufjs/utf8@1.1.0: resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + requiresBuild: true dev: false optional: true @@ -2661,6 +2709,7 @@ packages: /@smithy/abort-controller@2.0.1: resolution: {integrity: sha512-0s7XjIbsTwZyUW9OwXQ8J6x1UiA1TNCh60Vaw56nHahL7kUZsLhmTlWiaxfLkFtO2Utkj8YewcpHTYpxaTzO+w==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2670,6 +2719,7 @@ packages: /@smithy/config-resolver@2.0.1: resolution: {integrity: sha512-l83Pm7hV+8CBQOCmBRopWDtF+CURUJol7NsuPYvimiDhkC2F8Ba9T1imSFE+pD1UIJ9jlsDPAnZfPJT5cjnuEw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 '@smithy/util-config-provider': 2.0.0 @@ -2681,6 +2731,7 @@ packages: /@smithy/credential-provider-imds@2.0.1: resolution: {integrity: sha512-8VxriuRINNEfVZjEFKBY75y9ZWAx73DZ5K/u+3LmB6r8WR2h3NaFxFKMlwlq0uzNdGhD1ouKBn9XWEGYHKiPLw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/node-config-provider': 2.0.1 '@smithy/property-provider': 2.0.1 @@ -2692,6 +2743,7 @@ packages: /@smithy/eventstream-codec@2.0.1: resolution: {integrity: sha512-/IiNB7gQM2y2ZC/GAWOWDa8+iXfhr1g9Xe5979cQEOdCWDISvrAiv18cn3OtIQUhbYOR3gm7QtCpkq1to2takQ==} + requiresBuild: true dependencies: '@aws-crypto/crc32': 3.0.0 '@smithy/types': 2.0.2 @@ -2702,6 +2754,7 @@ packages: /@smithy/fetch-http-handler@2.0.1: resolution: {integrity: sha512-/SoU/ClazgcdOxgE4zA7RX8euiELwpsrKCSvulVQvu9zpmqJRyEJn8ZTWYFV17/eHOBdHTs9kqodhNhsNT+cUw==} + requiresBuild: true dependencies: '@smithy/protocol-http': 2.0.1 '@smithy/querystring-builder': 2.0.1 @@ -2714,6 +2767,7 @@ packages: /@smithy/hash-node@2.0.1: resolution: {integrity: sha512-oTKYimQdF4psX54ZonpcIE+MXjMUWFxLCNosjPkJPFQ9whRX0K/PFX/+JZGRQh3zO9RlEOEUIbhy9NO+Wha6hw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 '@smithy/util-buffer-from': 2.0.0 @@ -2724,6 +2778,7 @@ packages: /@smithy/invalid-dependency@2.0.1: resolution: {integrity: sha512-2q/Eb0AE662zwyMV+z+TL7deBwcHCgaZZGc0RItamBE8kak3MzCi/EZCNoFWoBfxgQ4jfR12wm8KKsSXhJzJtQ==} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2733,6 +2788,7 @@ packages: /@smithy/is-array-buffer@2.0.0: resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2741,6 +2797,7 @@ packages: /@smithy/middleware-content-length@2.0.1: resolution: {integrity: sha512-IZhRSk5GkVBcrKaqPXddBS2uKhaqwBgaSgbBb1OJyGsKe7SxRFbclWS0LqOR9fKUkDl+3lL8E2ffpo6EQg0igw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/protocol-http': 2.0.1 '@smithy/types': 2.0.2 @@ -2751,6 +2808,7 @@ packages: /@smithy/middleware-endpoint@2.0.1: resolution: {integrity: sha512-uz/KI1MBd9WHrrkVFZO4L4Wyv24raf0oR4EsOYEeG5jPJO5U+C7MZGLcMxX8gWERDn1sycBDqmGv8fjUMLxT6w==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/middleware-serde': 2.0.1 '@smithy/types': 2.0.2 @@ -2763,6 +2821,7 @@ packages: /@smithy/middleware-retry@2.0.1: resolution: {integrity: sha512-NKHF4i0gjSyjO6C0ZyjEpNqzGgIu7s8HOK6oT/1Jqws2Q1GynR1xV8XTUs1gKXeaNRzbzKQRewHHmfPwZjOtHA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/protocol-http': 2.0.1 '@smithy/service-error-classification': 2.0.0 @@ -2777,6 +2836,7 @@ packages: /@smithy/middleware-serde@2.0.1: resolution: {integrity: sha512-uKxPaC6ItH9ZXdpdqNtf8sda7GcU4SPMp0tomq/5lUg9oiMa/Q7+kD35MUrpKaX3IVXVrwEtkjCU9dogZ/RAUA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2786,6 +2846,7 @@ packages: /@smithy/middleware-stack@2.0.0: resolution: {integrity: sha512-31XC1xNF65nlbc16yuh3wwTudmqs6qy4EseQUGF8A/p2m/5wdd/cnXJqpniy/XvXVwkHPz/GwV36HqzHtIKATQ==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2794,6 +2855,7 @@ packages: /@smithy/node-config-provider@2.0.1: resolution: {integrity: sha512-Zoel4CPkKRTQ2XxmozZUfqBYqjPKL53/SvTDhJHj+VBSiJy6MXRav1iDCyFPS92t40Uh+Yi+Km5Ch3hQ+c/zSA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/property-provider': 2.0.1 '@smithy/shared-ini-file-loader': 2.0.1 @@ -2805,6 +2867,7 @@ packages: /@smithy/node-http-handler@2.0.1: resolution: {integrity: sha512-Zv3fxk3p9tsmPT2CKMsbuwbbxnq2gzLDIulxv+yI6aE+02WPYorObbbe9gh7SW3weadMODL1vTfOoJ9yFypDzg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/abort-controller': 2.0.1 '@smithy/protocol-http': 2.0.1 @@ -2817,6 +2880,7 @@ packages: /@smithy/property-provider@2.0.1: resolution: {integrity: sha512-pmJRyY9SF6sutWIktIhe+bUdSQDxv/qZ4mYr3/u+u45riTPN7nmRxPo+e4sjWVoM0caKFjRSlj3tf5teRFy0Vg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2826,6 +2890,7 @@ packages: /@smithy/protocol-http@2.0.1: resolution: {integrity: sha512-mrkMAp0wtaDEIkgRObWYxI1Kun1tm6Iu6rK+X4utb6Ah7Uc3Kk4VIWwK/rBHdYGReiLIrxFCB1rq4a2gyZnSgg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2835,6 +2900,7 @@ packages: /@smithy/querystring-builder@2.0.1: resolution: {integrity: sha512-bp+93WFzx1FojVEIeFPtG0A1pKsFdCUcZvVdZdRlmNooOUrz9Mm9bneRd8hDwAQ37pxiZkCOxopSXXRQN10mYw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 '@smithy/util-uri-escape': 2.0.0 @@ -2845,6 +2911,7 @@ packages: /@smithy/querystring-parser@2.0.1: resolution: {integrity: sha512-h+e7k1z+IvI2sSbUBG9Aq46JsgLl4UqIUl6aigAlRBj+P6ocNXpM6Yn1vMBw5ijtXeZbYpd1YvCxwDgdw3jhmg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2854,12 +2921,14 @@ packages: /@smithy/service-error-classification@2.0.0: resolution: {integrity: sha512-2z5Nafy1O0cTf69wKyNjGW/sNVMiqDnb4jgwfMG8ye8KnFJ5qmJpDccwIbJNhXIfbsxTg9SEec2oe1cexhMJvw==} engines: {node: '>=14.0.0'} + requiresBuild: true dev: false optional: true /@smithy/shared-ini-file-loader@2.0.1: resolution: {integrity: sha512-a463YiZrPGvM+F336rIF8pLfQsHAdCRAn/BiI/EWzg5xLoxbC7GSxIgliDDXrOu0z8gT3nhVsif85eU6jyct3A==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/types': 2.0.2 tslib: 2.6.1 @@ -2869,6 +2938,7 @@ packages: /@smithy/signature-v4@2.0.1: resolution: {integrity: sha512-jztv5Mirca42ilxmMDjzLdXcoAmRhZskGafGL49sRo5u7swEZcToEFrq6vtX5YMbSyTVrE9Teog5EFexY5Ff2Q==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/eventstream-codec': 2.0.1 '@smithy/is-array-buffer': 2.0.0 @@ -2884,6 +2954,7 @@ packages: /@smithy/smithy-client@2.0.1: resolution: {integrity: sha512-LHC5m6tYpEu1iNbONfvMbwtErboyTZJfEIPoD78Ei5MVr36vZQCaCla5mvo36+q/a2NAk2//fA5Rx3I1Kf7+lQ==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/middleware-stack': 2.0.0 '@smithy/types': 2.0.2 @@ -2895,6 +2966,7 @@ packages: /@smithy/types@2.0.2: resolution: {integrity: sha512-wcymEjIXQ9+NEfE5Yt5TInAqe1o4n+Nh+rh00AwoazppmUt8tdo6URhc5gkDcOYrcvlDVAZE7uG69nDpEGUKxw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2902,6 +2974,7 @@ packages: /@smithy/url-parser@2.0.1: resolution: {integrity: sha512-NpHVOAwddo+OyyIoujDL9zGL96piHWrTNXqltWmBvlUoWgt1HPyBuKs6oHjioyFnNZXUqveTOkEEq0U5w6Uv8A==} + requiresBuild: true dependencies: '@smithy/querystring-parser': 2.0.1 '@smithy/types': 2.0.2 @@ -2912,6 +2985,7 @@ packages: /@smithy/util-base64@2.0.0: resolution: {integrity: sha512-Zb1E4xx+m5Lud8bbeYi5FkcMJMnn+1WUnJF3qD7rAdXpaL7UjkFQLdmW5fHadoKbdHpwH9vSR8EyTJFHJs++tA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/util-buffer-from': 2.0.0 tslib: 2.6.1 @@ -2920,6 +2994,7 @@ packages: /@smithy/util-body-length-browser@2.0.0: resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2928,6 +3003,7 @@ packages: /@smithy/util-body-length-node@2.0.0: resolution: {integrity: sha512-ZV7Z/WHTMxHJe/xL/56qZwSUcl63/5aaPAGjkfynJm4poILjdD4GmFI+V+YWabh2WJIjwTKZ5PNsuvPQKt93Mg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2936,6 +3012,7 @@ packages: /@smithy/util-buffer-from@2.0.0: resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/is-array-buffer': 2.0.0 tslib: 2.6.1 @@ -2945,6 +3022,7 @@ packages: /@smithy/util-config-provider@2.0.0: resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2953,6 +3031,7 @@ packages: /@smithy/util-defaults-mode-browser@2.0.1: resolution: {integrity: sha512-w72Qwsb+IaEYEFtYICn0Do42eFju78hTaBzzJfT107lFOPdbjWjKnFutV+6GL/nZd5HWXY7ccAKka++C3NrjHw==} engines: {node: '>= 10.0.0'} + requiresBuild: true dependencies: '@smithy/property-provider': 2.0.1 '@smithy/types': 2.0.2 @@ -2964,6 +3043,7 @@ packages: /@smithy/util-defaults-mode-node@2.0.1: resolution: {integrity: sha512-dNF45caelEBambo0SgkzQ0v76m4YM+aFKZNTtSafy7P5dVF8TbjZuR2UX1A5gJABD9XK6lzN+v/9Yfzj/EDgGg==} engines: {node: '>= 10.0.0'} + requiresBuild: true dependencies: '@smithy/config-resolver': 2.0.1 '@smithy/credential-provider-imds': 2.0.1 @@ -2977,6 +3057,7 @@ packages: /@smithy/util-hex-encoding@2.0.0: resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2985,6 +3066,7 @@ packages: /@smithy/util-middleware@2.0.0: resolution: {integrity: sha512-eCWX4ECuDHn1wuyyDdGdUWnT4OGyIzV0LN1xRttBFMPI9Ff/4heSHVxneyiMtOB//zpXWCha1/SWHJOZstG7kA==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -2993,6 +3075,7 @@ packages: /@smithy/util-retry@2.0.0: resolution: {integrity: sha512-/dvJ8afrElasuiiIttRJeoS2sy8YXpksQwiM/TcepqdRVp7u4ejd9C4IQURHNjlfPUT7Y6lCDSa2zQJbdHhVTg==} engines: {node: '>= 14.0.0'} + requiresBuild: true dependencies: '@smithy/service-error-classification': 2.0.0 tslib: 2.6.1 @@ -3002,6 +3085,7 @@ packages: /@smithy/util-stream@2.0.1: resolution: {integrity: sha512-2a0IOtwIKC46EEo7E7cxDN8u2jwOiYYJqcFKA6rd5rdXqKakHT2Gc+AqHWngr0IEHUfW92zX12wRQKwyoqZf2Q==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/fetch-http-handler': 2.0.1 '@smithy/node-http-handler': 2.0.1 @@ -3017,6 +3101,7 @@ packages: /@smithy/util-uri-escape@2.0.0: resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: tslib: 2.6.1 dev: false @@ -3025,6 +3110,7 @@ packages: /@smithy/util-utf8@2.0.0: resolution: {integrity: sha512-rctU1VkziY84n5OXe3bPNpKR001ZCME2JCaBBFgtiM2hfKbHFudc/BkMuPab8hRbLd0j3vbnBTTZ1igBf0wgiQ==} engines: {node: '>=14.0.0'} + requiresBuild: true dependencies: '@smithy/util-buffer-from': 2.0.0 tslib: 2.6.1 @@ -3487,6 +3573,7 @@ packages: /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} + requiresBuild: true dev: false optional: true @@ -3576,6 +3663,7 @@ packages: /@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 @@ -3601,16 +3689,19 @@ packages: /@types/linkify-it@3.0.2: resolution: {integrity: sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==} + requiresBuild: true dev: false optional: true /@types/long@4.0.2: resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} + requiresBuild: true dev: false optional: true /@types/markdown-it@12.2.3: resolution: {integrity: sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==} + requiresBuild: true dependencies: '@types/linkify-it': 3.0.2 '@types/mdurl': 1.0.2 @@ -3619,6 +3710,7 @@ packages: /@types/mdurl@1.0.2: resolution: {integrity: sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==} + requiresBuild: true dev: false optional: true @@ -3628,6 +3720,7 @@ packages: /@types/minimatch@5.1.2: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + requiresBuild: true dev: false optional: true @@ -3681,6 +3774,7 @@ packages: /@types/rimraf@3.0.2: resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} + requiresBuild: true dependencies: '@types/glob': 8.1.0 '@types/node': 20.4.7 @@ -3736,6 +3830,7 @@ packages: /abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + requiresBuild: true dependencies: event-target-shim: 5.0.1 dev: false @@ -3816,6 +3911,7 @@ packages: /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + requiresBuild: true dependencies: debug: 4.3.4 transitivePeerDependencies: @@ -3903,6 +3999,7 @@ packages: /arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} + requiresBuild: true dev: false optional: true @@ -3925,6 +4022,7 @@ packages: /async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + requiresBuild: true dependencies: retry: 0.13.1 dev: false @@ -4042,6 +4140,7 @@ packages: /bignumber.js@9.1.1: resolution: {integrity: sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==} + requiresBuild: true dev: false optional: true @@ -4060,6 +4159,7 @@ packages: /bluebird@3.7.2: resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + requiresBuild: true dev: false optional: true @@ -4089,6 +4189,7 @@ packages: /bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + requiresBuild: true dev: false optional: true @@ -4195,6 +4296,7 @@ packages: /catharsis@0.9.0: resolution: {integrity: sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==} engines: {node: '>= 10'} + requiresBuild: true dependencies: lodash: 4.17.21 dev: false @@ -4280,6 +4382,7 @@ packages: /cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + requiresBuild: true dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 @@ -4372,6 +4475,7 @@ packages: /compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} + requiresBuild: true dependencies: mime-db: 1.52.0 dev: false @@ -4684,6 +4788,7 @@ packages: /duplexify@4.1.2: resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==} + requiresBuild: true dependencies: end-of-stream: 1.4.4 inherits: 2.0.4 @@ -4720,6 +4825,7 @@ packages: /end-of-stream@1.4.4: resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + requiresBuild: true dependencies: once: 1.4.0 dev: false @@ -4752,11 +4858,13 @@ packages: /ent@2.2.0: resolution: {integrity: sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==} + requiresBuild: true dev: false optional: true /entities@2.1.0: resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + requiresBuild: true dev: false optional: true @@ -4791,6 +4899,7 @@ packages: /escape-string-regexp@2.0.0: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + requiresBuild: true dev: false optional: true @@ -4802,6 +4911,7 @@ packages: resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} engines: {node: '>=4.0'} hasBin: true + requiresBuild: true dependencies: esprima: 4.0.1 estraverse: 4.3.0 @@ -4915,6 +5025,7 @@ packages: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + requiresBuild: true dev: false optional: true @@ -4933,6 +5044,7 @@ packages: /estraverse@4.3.0: resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} engines: {node: '>=4.0'} + requiresBuild: true dev: false optional: true @@ -4960,6 +5072,7 @@ packages: /event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + requiresBuild: true dev: false optional: true @@ -5049,6 +5162,7 @@ packages: /extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + requiresBuild: true dev: false optional: true @@ -5077,12 +5191,14 @@ packages: /fast-text-encoding@1.0.6: resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} + requiresBuild: true dev: false optional: true /fast-xml-parser@4.2.5: resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} hasBin: true + requiresBuild: true dependencies: strnum: 1.0.5 dev: false @@ -5091,6 +5207,7 @@ packages: /fast-xml-parser@4.2.7: resolution: {integrity: sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==} hasBin: true + requiresBuild: true dependencies: strnum: 1.0.5 dev: false @@ -5277,6 +5394,7 @@ packages: /gaxios@5.1.3: resolution: {integrity: sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==} engines: {node: '>=12'} + requiresBuild: true dependencies: extend: 3.0.2 https-proxy-agent: 5.0.1 @@ -5291,6 +5409,7 @@ packages: /gcp-metadata@5.3.0: resolution: {integrity: sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==} engines: {node: '>=12'} + requiresBuild: true dependencies: gaxios: 5.1.3 json-bigint: 1.0.0 @@ -5362,6 +5481,7 @@ packages: /glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + requiresBuild: true dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -5396,6 +5516,7 @@ packages: /google-auth-library@8.9.0: resolution: {integrity: sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg==} engines: {node: '>=12'} + requiresBuild: true dependencies: arrify: 2.0.1 base64-js: 1.5.1 @@ -5416,6 +5537,7 @@ packages: resolution: {integrity: sha512-g/lcUjGcB6DSw2HxgEmCDOrI/CByOwqRvsuUvNalHUK2iPPPlmAIpbMbl62u0YufGMr8zgE3JL7th6dCb1Ry+w==} engines: {node: '>=12'} hasBin: true + requiresBuild: true dependencies: '@grpc/grpc-js': 1.8.21 '@grpc/proto-loader': 0.7.8 @@ -5442,6 +5564,7 @@ packages: resolution: {integrity: sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ==} engines: {node: '>=12.0.0'} hasBin: true + requiresBuild: true dependencies: node-forge: 1.3.1 dev: false @@ -5455,6 +5578,7 @@ packages: /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + requiresBuild: true dev: false optional: true @@ -5464,6 +5588,7 @@ packages: /gtoken@6.1.2: resolution: {integrity: sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ==} engines: {node: '>=12.0.0'} + requiresBuild: true dependencies: gaxios: 5.1.3 google-p12-pem: 4.0.1 @@ -5547,6 +5672,7 @@ packages: /http-proxy-agent@5.0.0: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + requiresBuild: true dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 @@ -5559,6 +5685,7 @@ packages: /https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + requiresBuild: true dependencies: agent-base: 6.0.2 debug: 4.3.4 @@ -5723,6 +5850,7 @@ packages: /is-stream-ended@0.1.4: resolution: {integrity: sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==} + requiresBuild: true dev: false optional: true @@ -5784,6 +5912,7 @@ packages: /js2xmlparser@4.0.2: resolution: {integrity: sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==} + requiresBuild: true dependencies: xmlcreate: 2.0.4 dev: false @@ -5793,6 +5922,7 @@ packages: resolution: {integrity: sha512-e8cIg2z62InH7azBBi3EsSEqrKx+nUtAS5bBcYTSpZFA+vhNPyhv8PTFZ0WsjOPDj04/dOLlm08EDcQJDqaGQg==} engines: {node: '>=12.0.0'} hasBin: true + requiresBuild: true dependencies: '@babel/parser': 7.22.7 '@jsdoc/salty': 0.2.5 @@ -5825,6 +5955,7 @@ packages: /json-bigint@1.0.0: resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + requiresBuild: true dependencies: bignumber.js: 9.1.1 dev: false @@ -5890,6 +6021,7 @@ packages: /jwa@2.0.0: resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + requiresBuild: true dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 @@ -5920,6 +6052,7 @@ packages: /jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + requiresBuild: true dependencies: jwa: 2.0.0 safe-buffer: 5.2.1 @@ -5938,6 +6071,7 @@ packages: /klaw@3.0.0: resolution: {integrity: sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==} + requiresBuild: true dependencies: graceful-fs: 4.2.11 dev: false @@ -5957,6 +6091,7 @@ packages: /levn@0.3.0: resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} engines: {node: '>= 0.8.0'} + requiresBuild: true dependencies: prelude-ls: 1.1.2 type-check: 0.3.2 @@ -5980,6 +6115,7 @@ packages: /linkify-it@3.0.3: resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + requiresBuild: true dependencies: uc.micro: 1.0.6 dev: false @@ -6011,6 +6147,7 @@ packages: /lodash.camelcase@4.3.0: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + requiresBuild: true dev: false optional: true @@ -6089,11 +6226,13 @@ packages: /long@4.0.0: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} + requiresBuild: true dev: false optional: true /long@5.2.3: resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + requiresBuild: true dev: false optional: true @@ -6156,6 +6295,7 @@ packages: /markdown-it-anchor@8.6.7(@types/markdown-it@12.2.3)(markdown-it@12.3.2): resolution: {integrity: sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==} + requiresBuild: true peerDependencies: '@types/markdown-it': '*' markdown-it: '*' @@ -6168,6 +6308,7 @@ packages: /markdown-it@12.3.2: resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} hasBin: true + requiresBuild: true dependencies: argparse: 2.0.1 entities: 2.1.0 @@ -6192,6 +6333,7 @@ packages: resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} engines: {node: '>= 12'} hasBin: true + requiresBuild: true dev: false optional: true @@ -6210,6 +6352,7 @@ packages: /memory-pager@1.5.0: resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + requiresBuild: true dev: false optional: true @@ -6262,6 +6405,7 @@ packages: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true + requiresBuild: true dev: false optional: true @@ -6289,6 +6433,7 @@ packages: /minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} + requiresBuild: true dependencies: brace-expansion: 2.0.1 dev: false @@ -6296,6 +6441,7 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + requiresBuild: true dev: false optional: true @@ -6303,6 +6449,7 @@ packages: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true + requiresBuild: true dev: false optional: true @@ -6413,9 +6560,17 @@ packages: engines: {node: '>= 0.6'} dev: false + /node-cron@3.0.2: + resolution: {integrity: sha512-iP8l0yGlNpE0e6q1o185yOApANRe47UPbLf4YxfbiNHt/RU5eBcGB/e0oudruheSf+LQeDMezqC5BVAb5wwRcQ==} + engines: {node: '>=6.0.0'} + dependencies: + uuid: 8.3.2 + dev: false + /node-fetch@2.6.12: resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} engines: {node: 4.x || >=6.0.0} + requiresBuild: true peerDependencies: encoding: ^0.1.0 peerDependenciesMeta: @@ -6493,6 +6648,7 @@ packages: /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} + requiresBuild: true dev: false optional: true @@ -6546,6 +6702,7 @@ packages: /optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} engines: {node: '>= 0.8.0'} + requiresBuild: true dependencies: deep-is: 0.1.4 fast-levenshtein: 2.0.6 @@ -6714,6 +6871,7 @@ packages: /prelude-ls@1.1.2: resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} engines: {node: '>= 0.8.0'} + requiresBuild: true dev: false optional: true @@ -6875,6 +7033,7 @@ packages: /proto3-json-serializer@1.1.1: resolution: {integrity: sha512-AwAuY4g9nxx0u52DnSMkqqgyLHaW/XaPLtaAo3y/ZCfeaQB/g4YDH4kb8Wc/mWzWvu0YjOznVnfn373MVZZrgw==} engines: {node: '>=12.0.0'} + requiresBuild: true dependencies: protobufjs: 7.2.4 dev: false @@ -6884,6 +7043,7 @@ packages: resolution: {integrity: sha512-VPWMgIcRNyQwWUv8OLPyGQ/0lQY/QTQAVN5fh+XzfDwsVw1FZ2L3DM/bcBf8WPiRz2tNpaov9lPZfNcmNo6LXA==} engines: {node: '>=12.0.0'} hasBin: true + requiresBuild: true peerDependencies: protobufjs: ^7.0.0 dependencies: @@ -7300,6 +7460,7 @@ packages: /requizzle@0.2.4: resolution: {integrity: sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==} + requiresBuild: true dependencies: lodash: 4.17.21 dev: false @@ -7337,6 +7498,7 @@ packages: /retry-request@5.0.2: resolution: {integrity: sha512-wfI3pk7EE80lCIXprqh7ym48IHYdwmAAzESdbU8Q9l7pnRCk9LEhpbOTNKjz6FARLm/Bl5m+4F0ABxOkYUujSQ==} engines: {node: '>=12'} + requiresBuild: true dependencies: debug: 4.3.4 extend: 3.0.2 @@ -7348,6 +7510,7 @@ packages: /retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + requiresBuild: true dev: false optional: true @@ -7618,6 +7781,7 @@ packages: /sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + requiresBuild: true dependencies: memory-pager: 1.5.0 dev: false @@ -7634,6 +7798,7 @@ packages: /stream-events@1.0.5: resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} + requiresBuild: true dependencies: stubs: 3.0.0 dev: false @@ -7641,6 +7806,7 @@ packages: /stream-shift@1.0.1: resolution: {integrity: sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==} + requiresBuild: true dev: false optional: true @@ -7670,11 +7836,13 @@ packages: /strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + requiresBuild: true dev: false optional: true /stubs@3.0.0: resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} + requiresBuild: true dev: false optional: true @@ -7792,6 +7960,7 @@ packages: /teeny-request@8.0.3: resolution: {integrity: sha512-jJZpA5He2y52yUhA7pyAGZlgQpcB+xLjcN0eUFxr9c8hP/H7uOXbBNVo/O0C/xVfJLJs680jvkFgVJEEvk9+ww==} engines: {node: '>=12'} + requiresBuild: true dependencies: http-proxy-agent: 5.0.0 https-proxy-agent: 5.0.1 @@ -7848,6 +8017,7 @@ packages: /tmp@0.2.1: resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} engines: {node: '>=8.17.0'} + requiresBuild: true dependencies: rimraf: 3.0.2 dev: false @@ -7878,6 +8048,7 @@ packages: /tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + requiresBuild: true dev: false optional: true @@ -7895,6 +8066,7 @@ packages: /tslib@1.14.1: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + requiresBuild: true dev: false optional: true @@ -7905,6 +8077,7 @@ packages: /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} + requiresBuild: true dependencies: prelude-ls: 1.1.2 dev: false @@ -7946,6 +8119,7 @@ packages: resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} engines: {node: '>=0.8.0'} hasBin: true + requiresBuild: true dev: false optional: true @@ -7962,6 +8136,7 @@ packages: /underscore@1.13.6: resolution: {integrity: sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==} + requiresBuild: true dev: false optional: true @@ -8115,6 +8290,7 @@ packages: /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + requiresBuild: true dev: false optional: true @@ -8147,6 +8323,7 @@ packages: /whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + requiresBuild: true dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 @@ -8213,6 +8390,7 @@ packages: /word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + requiresBuild: true dev: false optional: true @@ -8259,6 +8437,7 @@ packages: /xmlcreate@2.0.4: resolution: {integrity: sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==} + requiresBuild: true dev: false optional: true @@ -8299,6 +8478,7 @@ packages: /yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + requiresBuild: true dev: false optional: true @@ -8328,6 +8508,7 @@ packages: /yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + requiresBuild: true dependencies: cliui: 8.0.1 escalade: 3.1.1 @@ -8342,3 +8523,4 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + requiresBuild: true diff --git a/src/lottery/index.js b/src/lottery/index.js new file mode 100644 index 00000000..d06be299 --- /dev/null +++ b/src/lottery/index.js @@ -0,0 +1,41 @@ +const express = require("express"); +const { + eventStatusModel, + eventModel, + itemModel, + transactionModel, +} = require("./modules/stores/mongo"); + +const { buildResource } = require("../modules/adminResource"); +const { instagramRewardAction } = require("./modules/admin"); + +// [Routes] 기존 docs 라우터의 docs extend +require("./routes/docs")(); + +// [Middleware] 목표 달성 여부 검증 +const checkReward = (req, res, next) => { + next(); +}; + +const lotteryRouter = express.Router(); + +// [Middleware] 모든 API 요청에 대하여 origin 검증 +lotteryRouter.use(require("../middlewares/originValidator")); + +// [Router] APIs +lotteryRouter.use("/global-state", require("./routes/globalState")); +lotteryRouter.use("/transactions", require("./routes/transactions")); +lotteryRouter.use("/items", require("./routes/items")); + +const eventStatusResource = buildResource([instagramRewardAction])( + eventStatusModel +); +const otherResources = [eventModel, itemModel, transactionModel].map( + buildResource() +); + +module.exports = { + checkReward, + lotteryRouter, + resources: [eventStatusResource, ...otherResources], +}; diff --git a/src/lottery/modules/admin.js b/src/lottery/modules/admin.js new file mode 100644 index 00000000..a112323a --- /dev/null +++ b/src/lottery/modules/admin.js @@ -0,0 +1,72 @@ +const { useUserCreditAmount } = require("./credit"); +const { transactionModel } = require("./stores/mongo"); +const { recordAction } = require("../../modules/adminResource"); +const { eventEnv } = require("../../../loadenv"); + +/** eventId가 없는 경우 null이 아닌 undefined를 넣어야 합니다. */ +const creditTransfer = async (userId, amount, eventId, comment) => { + const user = await useUserCreditAmount(userId); + await user.creditUpdate(amount); + + const transaction = new transactionModel({ + type: "get", + amount, + userId, + eventId, + comment, + }); + await transaction.save(); + + return transaction._id; +}; + +/** itemId가 없는 경우 null이 아닌 undefined를 넣어야 합니다. */ +const creditWithdraw = async (userId, amount, itemId, comment) => { + const user = await useUserCreditAmount(userId); + await user.creditUpdate(-amount); + + const transaction = new transactionModel({ + type: "use", + amount, + userId, + itemId, + comment, + }); + await transaction.save(); + + return transaction._id; +}; + +const instagramRewardActionHandler = async (req, res, context) => { + const transactionId = await creditTransfer( + context?.record?.params?.userId, + eventEnv.instagramReward, + eventEnv.instagramEventId, + eventEnv.instagramComment + ); + + let record = context.record.toJSON(context.currentAdmin); + record.params.creditAmount += eventEnv.instagramReward; + + return { + record, + transactionId, + }; +}; +const instagramRewardActionLogs = [ + "update", + { + action: "create", + target: (res, req, context) => `Transaction(_id = ${res.transactionId})`, + }, +]; + +const instagramRewardAction = recordAction( + "instagramReward", + instagramRewardActionHandler, + instagramRewardActionLogs +); + +module.exports = { + instagramRewardAction, +}; diff --git a/src/lottery/modules/credit.js b/src/lottery/modules/credit.js new file mode 100644 index 00000000..b55613a0 --- /dev/null +++ b/src/lottery/modules/credit.js @@ -0,0 +1,24 @@ +const { eventStatusModel } = require("../modules/stores/mongo"); + +const useUserCreditAmount = async (userId) => { + const eventStatus = await eventStatusModel.findOne({ userId }); + if (!eventStatus) return null; + + return { + creditAmount: eventStatus.creditAmount, + creditUpdate: async (delta) => { + await eventStatusModel.updateOne( + { _id: eventStatus._id }, + { + $inc: { + creditAmount: delta, + }, + } + ); + }, + }; +}; + +module.exports = { + useUserCreditAmount, +}; diff --git a/src/lottery/modules/stores/mongo.js b/src/lottery/modules/stores/mongo.js new file mode 100644 index 00000000..c6b38d03 --- /dev/null +++ b/src/lottery/modules/stores/mongo.js @@ -0,0 +1,141 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; + +const integerValidator = { + validator: Number.isInteger, + message: "{VALUE} is not an integer value", +}; + +const eventStatusSchema = Schema({ + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + eventList: { + type: [Schema.Types.ObjectId], + default: [], + ref: "Event", + }, + creditAmount: { + type: Number, + default: 0, + min: 0, + validate: integerValidator, + }, +}); + +const eventSchema = Schema({ + name: { + type: String, + required: true, + }, + rewardAmount: { + type: Number, + required: true, + min: 0, + validate: integerValidator, + }, + maxCount: { + type: Number, + default: 1, + min: 0, + validate: integerValidator, + }, + expireat: { + type: Date, + required: true, + }, + isDisabled: { + type: Boolean, + default: false, + }, +}); + +const itemSchema = Schema({ + name: { + type: String, + required: true, + }, + imageUrl: { + type: String, + required: true, + }, + price: { + type: Number, + required: true, + min: 0, + validate: integerValidator, + }, + description: { + type: String, + required: true, + }, + isDisabled: { + type: Boolean, + default: false, + }, + stock: { + type: Number, + required: true, + min: 0, + validate: integerValidator, + }, + itemType: { + type: Number, + enum: [0, 1, 2, 3], + required: true, + }, + isRandomItem: { + type: Boolean, + required: true, + }, + randomWeight: { + type: Number, + required: true, + min: 0, + validate: integerValidator, + }, +}); + +const transactionSchema = Schema({ + type: { + type: String, + enum: ["get", "use"], + required: true, + }, + amount: { + type: Number, + required: true, + min: 0, + validate: integerValidator, + }, + userId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + eventId: { + type: Schema.Types.ObjectId, + ref: "Event", + }, + itemId: { + type: Schema.Types.ObjectId, + ref: "Item", + }, + comment: { + type: String, + required: true, + }, +}); +transactionSchema.set("timestamps", { + createdAt: "doneat", + updatedAt: false, +}); + +module.exports = { + eventStatusModel: mongoose.model("EventStatus", eventStatusSchema), + eventModel: mongoose.model("Event", eventSchema), + itemModel: mongoose.model("Item", itemSchema), + transactionModel: mongoose.model("Transaction", transactionSchema), +}; diff --git a/src/lottery/routes/docs/globalState.js b/src/lottery/routes/docs/globalState.js new file mode 100644 index 00000000..8499e8fb --- /dev/null +++ b/src/lottery/routes/docs/globalState.js @@ -0,0 +1,52 @@ +const { eventMode } = require("../../../../loadenv"); +const apiPrefix = `/events/${eventMode}/global-state`; + +const globalStateDocs = {}; +globalStateDocs[`${apiPrefix}/`] = { + get: { + tags: [`${apiPrefix}`], + summary: "Frontend에서 Global state로 관리하는 정보 반환", + description: + "유저의 재화 개수, 이벤트 달성 상태, 추첨권 개수 등 Frontend에서 Global state로 관리할 정보를 가져옵니다. 유저에 대한 EventStatus Document가 없을 경우 새롭게 생성하며, 유일한 생성 지점입니다.", + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: { + type: "object", + properties: { + creditAmount: { + type: "number", + description: "재화 개수. 0 이상입니다.", + example: 10000, + }, + eventStatus: { + type: "array", + description: + "유저가 달성한 이벤트의 배열. 여러 번 달성할 수 있는 이벤트의 경우 배열 내에 같은 이벤트가 여러 번 포함될 수 있습니다.", + items: { + type: "string", + description: "Event의 ObjectId", + }, + }, + ticket1Amount: { + type: "number", + description: "추첨권 (1)의 개수. 0 이상입니다.", + example: 10, + }, + ticket2Amount: { + type: "number", + description: "추첨권 (2)의 개수. 0 이상입니다.", + example: 10, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +module.exports = globalStateDocs; diff --git a/src/lottery/routes/docs/index.js b/src/lottery/routes/docs/index.js new file mode 100644 index 00000000..fa845079 --- /dev/null +++ b/src/lottery/routes/docs/index.js @@ -0,0 +1,22 @@ +const swaggerUi = require("swagger-ui-express"); +const swaggerDocs = require("../../../routes/docs/swaggerDocs"); +const eventSwaggerDocs = require("./swaggerDocs"); + +swaggerDocs.tags = [...swaggerDocs.tags, ...eventSwaggerDocs.tags]; + +swaggerDocs.paths = { + ...swaggerDocs.paths, + ...eventSwaggerDocs.paths, +}; + +swaggerDocs.components.schemas = { + ...swaggerDocs.components.schemas, + ...eventSwaggerDocs.components.schemas, +}; + +/** 기존 docs 라우터에 이벤트 API docs를 추가합니다. */ +const appendEventDocs = () => { + swaggerUi.setup(swaggerDocs, { explorer: true }); +}; + +module.exports = appendEventDocs; diff --git a/src/lottery/routes/docs/items.js b/src/lottery/routes/docs/items.js new file mode 100644 index 00000000..865a3902 --- /dev/null +++ b/src/lottery/routes/docs/items.js @@ -0,0 +1,147 @@ +const { eventMode } = require("../../../../loadenv"); +const apiPrefix = `/events/${eventMode}/items`; + +const itemsDocs = {}; +itemsDocs[`${apiPrefix}/list`] = { + get: { + tags: [`${apiPrefix}`], + summary: "상점에서 판매하는 모든 상품의 목록 반환", + description: + "상점에서 판매하는 모든 상품의 목록을 가져옵니다. 매진된 상품도 가져옵니다.", + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: { + type: "object", + properties: { + items: { + type: "array", + description: "Item의 배열", + items: { + type: "object", + properties: { + _id: { + type: "string", + description: "Item의 ObjectId", + example: "OBJECT ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "랜덤 상자", + }, + imageUrl: { + type: "string", + description: "이미지 썸네일 URL", + example: "THUMBNAIL URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상입니다.", + example: 400, + }, + description: { + type: "string", + description: "상품의 설명", + example: + "랜덤으로 상품이 나오는 상자입니다. 확률은 다음과 같습니다: 진짜송편 100%, 치킨 0%, ...", + }, + isDisabled: { + type: "boolean", + description: "판매 중지 여부", + example: false, + }, + stock: { + type: "number", + description: "남은 상품 재고. 0 이상입니다.", + example: 10, + }, + itemType: { + type: "number", + description: + "아이템 유형. 0: 티켓아님, 1:티켓 타입1, 2: 티켓 타입 2, 3: 랜덤박스", + example: 0, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; +itemsDocs[`${apiPrefix}/purchase/:itemId`] = { + post: { + tags: [`${apiPrefix}`], + summary: "상품 구매", + description: "상품을 구매합니다.", + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: { + type: "object", + required: ["result"], + properties: { + result: { + type: "boolean", + description: "성공 여부. 항상 true입니다.", + example: true, + }, + reward: { + type: "object", + description: "랜덤박스를 구입한 경우에만 포함됩니다.", + properties: { + _id: { + type: "string", + description: "Item의 ObjectId", + example: "OBJECT ID", + }, + name: { + type: "string", + description: "상품의 이름", + example: "진짜송편", + }, + imageUrl: { + type: "string", + description: "이미지 썸네일 URL", + example: "THUMBNAIL URL", + }, + price: { + type: "number", + description: "상품의 가격. 0 이상입니다.", + example: 400, + }, + description: { + type: "string", + description: "상품의 설명", + example: "맛있는 송편입니다.", + }, + isDisabled: { + type: "boolean", + description: "판매 중지 여부", + example: false, + }, + stock: { + type: "number", + description: "남은 상품 재고. 0 이상입니다.", + example: 10, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; + +module.exports = itemsDocs; diff --git a/src/lottery/routes/docs/swaggerDocs.js b/src/lottery/routes/docs/swaggerDocs.js new file mode 100644 index 00000000..4896f122 --- /dev/null +++ b/src/lottery/routes/docs/swaggerDocs.js @@ -0,0 +1,27 @@ +const { eventMode } = require("../../../../loadenv"); +const globalStateDocs = require("./globalState"); +const itemsDocs = require("./items"); + +const apiPrefix = `/events/${eventMode}`; + +const eventSwaggerDocs = { + tags: [ + { + name: `${apiPrefix}/global-state`, + description: "이벤트 - Global State 관련 API", + }, + { + name: `${apiPrefix}/items`, + description: "이벤트 - 아이템 관련 API", + }, + ], + paths: { + ...globalStateDocs, + ...itemsDocs, + }, + components: { + schemas: {}, + }, +}; + +module.exports = eventSwaggerDocs; diff --git a/src/lottery/routes/globalState.js b/src/lottery/routes/globalState.js new file mode 100644 index 00000000..f0f75406 --- /dev/null +++ b/src/lottery/routes/globalState.js @@ -0,0 +1,11 @@ +const express = require("express"); + +const router = express.Router(); +const globalStateHandlers = require("../services/globalState"); + +// 라우터 접근 시 로그인 필요 +router.use(require("../../middlewares/auth")); + +router.get("/", globalStateHandlers.getUserGlobalStateHandler); + +module.exports = router; diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js new file mode 100644 index 00000000..539288bc --- /dev/null +++ b/src/lottery/routes/items.js @@ -0,0 +1,10 @@ +const express = require("express"); + +const router = express.Router(); +const itemsHandlers = require("../services/items"); +const auth = require("../../middlewares/auth"); + +router.get("/list", itemsHandlers.listHandler); +router.post("/purchase/:itemId", auth, itemsHandlers.purchaseHandler); + +module.exports = router; diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js new file mode 100644 index 00000000..391e0cf6 --- /dev/null +++ b/src/lottery/routes/transactions.js @@ -0,0 +1,11 @@ +const express = require("express"); + +const router = express.Router(); +const transactionsHandlers = require("../services/transactions"); + +// 라우터 접근 시 로그인 필요 +router.use(require("../../middlewares/auth")); + +router.get("/", transactionsHandlers.getUserTransactionsHandler); + +module.exports = router; diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js new file mode 100644 index 00000000..37e9731c --- /dev/null +++ b/src/lottery/services/globalState.js @@ -0,0 +1,57 @@ +const { + eventStatusModel, + transactionModel, + itemModel, +} = require("../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +const getUserGlobalStateHandler = async (req, res) => { + try { + let eventStatus = await eventStatusModel.findOne({ userId: req.userOid }); + if (!eventStatus) { + // User마다 EventStatus를 가져야 하고, 현재 Taxi에는 회원 탈퇴 시스템이 없으므로, EventStatus가 없으면 새롭게 생성하도록 구현합니다. + // EventStatus의 생성은 이곳에서만 이루어집니다!! + eventStatus = new eventStatusModel({ + userId: req.userOid, + }); + await eventStatus.save(); + } + + let ticket1Amount = 0; + let ticket2Amount = 0; + + const itemPurchaseTransactions = await transactionModel.find({ + userId: req.userOid, + type: "use", + itemId: { + $exists: true, + $ne: null, + }, + }); + await Promise.all( + itemPurchaseTransactions.map(async (purchase) => { + const item = await itemModel.findOne({ _id: purchase.itemId }); + + if (item.itemType === 1) { + ticket1Amount++; + } else if (item.itemType === 2) { + ticket2Amount++; + } + }) + ); + + res.json({ + creditAmount: eventStatus.creditAmount, + eventStatus: eventStatus.eventList.map((id) => id.toString()), + ticket1Amount, + ticket2Amount, + }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "GlobalState/ : internal server error" }); + } +}; + +module.exports = { + getUserGlobalStateHandler, +}; diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js new file mode 100644 index 00000000..94029823 --- /dev/null +++ b/src/lottery/services/items.js @@ -0,0 +1,162 @@ +const { itemModel, transactionModel } = require("../modules/stores/mongo"); +const logger = require("../../modules/logger"); +const { useUserCreditAmount } = require("../modules/credit"); + +const getRandomItem = async (req, depth) => { + if (depth >= 10) return null; + + const items = await itemModel.find({ + isRandomItem: true, + stock: { $gt: 0 }, + isDisabled: false, + }); + const randomItems = items + .map((item) => { + return Array(item.randomWeight).fill(item); + }) + .reduce((a, b) => a.concat(b), []); + + logger.info( + `유저 "${req.userOid}"에 의해 getRandomItem(depth=${depth})가 호출되었습니다.` + ); + logger.info( + `유저 "${req.userOid}"의 랜덤박스 확률 정보입니다: [${randomItems + .map((item) => item._id.toString()) + .join(",")}]` + ); + + if (randomItems.length === 0) return null; + + const randomItem = + randomItems[Math.floor(Math.random() * randomItems.length)]; + try { + const newRandomItem = await itemModel.findOneAndUpdate( + { _id: randomItem._id }, + { + $inc: { + stock: -1, + }, + }, + { + runValidators: true, + new: true, + } + ); + + const transaction = new transactionModel({ + type: "use", + amount: 0, + userId: req.userOid, + itemId: randomItem._id, + comment: `랜덤박스에서 ${randomItem.name} 획득 - 0개 차감`, + }); + await transaction.save(); + + return newRandomItem; + } catch (err) { + logger.warn( + `유저 "${req.userOid}"의 랜덤박스 추첨이 실패했습니다. 오류 정보: ${err}` + ); + + return await getRandomItem(depth + 1); + } +}; + +const listHandler = async (_, res) => { + try { + const items = await itemModel.find( + {}, + "name imageUrl price description isDisabled stock itemType" + ); + res.json({ items }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/List : internal server error" }); + } +}; + +const purchaseHandler = async (req, res) => { + try { + const { itemId } = req.params; + const item = await itemModel.findOne({ _id: itemId }); + if (!item) + return res.status(400).json({ error: "Items/Purchase : invalid Item" }); + + const user = await useUserCreditAmount(req.userOid); + if (!user) + return res + .status(400) + .json({ error: "Items/Purchase : invalid EventStatus" }); + + // 구매 가능 조건: 크레딧이 충분하며, 재고가 남아있으며, 판매 중인 아이템이어야 합니다. + if (item.isDisabled) + return res.status(400).json({ error: "Items/Purchase : disabled item" }); + if (user.creditAmount < item.price) + return res + .status(400) + .json({ error: "Items/Purchase : not enough credit" }); + if (item.stock <= 0) + return res + .status(400) + .json({ error: "Items/Purchase : item out of stock" }); + + // 1단계: 재고를 차감합니다. + // 재고가 차감됐으나 유저 크레딧이 차감되지 않은 경우, 나중에 Transaction 기록 분석을 통해 오류 복구가 가능합니다. + // 하지만 유저 크레딧이 차감됐으나 재고가 차감되지 않은 경우, 다른 유저가 품절된 상품을 구입할 수 있게 되고, 이는 다수의 유저에게 불편을 야기할 수 있습니다. + await itemModel.updateOne( + { _id: item._id }, + { + $inc: { + stock: -1, + }, + }, + { + runValidators: true, + } + ); + + // 2단계: 유저의 크레딧을 차감합니다. + await user.creditUpdate(-item.price); + + // 3단계: Transaction을 추가합니다. + // Transaction은 가장 마지막에 추가해야 다른 문서와의 불일치를 감지할 수 있습니다. + const transaction = new transactionModel({ + type: "use", + amount: item.price, + userId: req.userOid, + itemId: item._id, + comment: `${item.name} 구입 - ${item.price}개 차감`, + }); + await transaction.save(); + + // 4단계: 랜덤박스인 경우 아이템을 추첨합니다. + if (item.itemType !== 3) return res.json({ result: true }); + + const randomItem = await getRandomItem(req, 0); + if (!randomItem) + return res + .status(500) + .json({ error: "Items/Purchase : random box error" }); + + res.json({ + result: true, + reward: { + _id: randomItem._id, + name: randomItem.name, + imageUrl: randomItem.imageUrl, + price: randomItem.price, + description: randomItem.description, + isDisabled: randomItem.isDisabled, + stock: randomItem.stock, + }, + }); + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Items/Purchase : internal server error" }); + } +}; + +module.exports = { + listHandler, + purchaseHandler, +}; diff --git a/src/lottery/services/transactions.js b/src/lottery/services/transactions.js new file mode 100644 index 00000000..85f6a993 --- /dev/null +++ b/src/lottery/services/transactions.js @@ -0,0 +1,19 @@ +const { transactionModel } = require("../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +const getUserTransactionsHandler = async (req, res) => { + try { + const transactions = await transactionModel.find( + { userId: req.userOid }, + "-userId" + ); + res.json({ transactions }); // userId는 이미 Frontend에서 알고 있고, 중복되는 값이므로 제외합니다. + } catch (err) { + logger.error(err); + res.status(500).json({ error: "Transactions/ : internal server error" }); + } +}; + +module.exports = { + getUserTransactionsHandler, +}; diff --git a/src/middlewares/auth.js b/src/middlewares/auth.js index 3e3e7f35..e521f9f4 100644 --- a/src/middlewares/auth.js +++ b/src/middlewares/auth.js @@ -8,8 +8,9 @@ const authMiddleware = (req, res, next) => { error: "not logged in", }); } else { - const { id } = getLoginInfo(req); + const { id, oid } = getLoginInfo(req); req.userId = id; + req.userOid = oid; next(); } }; diff --git a/src/modules/adminResource.js b/src/modules/adminResource.js new file mode 100644 index 00000000..655e928b --- /dev/null +++ b/src/modules/adminResource.js @@ -0,0 +1,103 @@ +const { buildFeature } = require("adminjs"); +const { adminLogModel } = require("./stores/mongo"); + +const createLog = async (req, action, target) => { + const newLog = new adminLogModel({ + user: req.userOid, // Log 취급자 User + time: req.timestamp, // Log 발생 시각 + ip: req.clientIP, // 접속 IP 주소 + target, // 처리한 정보주체 정보 + action, // 수행 업무 + }); + await newLog.save(); +}; + +const generateTarget = (context, isList) => { + const modelName = context?.resource?.MongooseModel?.modelName; + const recordLength = `(length = ${context?.records?.length})`; + const recordId = `(_id = ${context?.record?.params?._id})`; + + return isList + ? `List<${modelName}>${recordLength}` + : `${modelName}${recordId}`; +}; + +const defaultActionAfterHandler = (actionName) => async (res, req, context) => { + if ( + ["new", "edit", "bulkDelete"].includes(actionName) && + req.method !== "post" + ) + return res; // 왜 필요한건지는 잘 모르겠으나, 기존에 존재하던 코드라 지우지 않고 유지합니다. + + const [action, isList] = { + list: ["read", true], + show: ["read", false], + new: ["create", false], + edit: ["update", false], + delete: ["delete", false], + bulkDelete: ["delete", true], + }?.[actionName]; + + const target = generateTarget(context, isList); + await createLog(req, action, target); + + return res; +}; + +const defaultActionLogFeature = buildFeature({ + actions: ["list", "show", "new", "edit", "delete", "bulkDelete"].reduce( + (before, actionName) => ({ + ...before, + [actionName]: { + after: defaultActionAfterHandler(actionName), + }, + }), + {} + ), +}); + +const recordActionAfterHandler = (actions) => async (res, req, context) => { + const actionsWrapper = Array.isArray(actions) ? actions : [actions]; + for (const action of actionsWrapper) { + if (typeof action === "string") { + const target = generateTarget(context, false); + await createLog(req, action, target); + } else { + await createLog(req, action.action, action.target(res, req, context)); + } + } + + return res; +}; + +const recordAction = (actionName, handler, logActions) => ({ + actionName, + actionType: "record", + component: false, + handler, + after: recordActionAfterHandler(logActions), +}); + +const buildResource = + (actions = [], features = []) => + (resource) => ({ + resource, + options: { + actions: actions.reduce( + (before, action) => ({ + ...before, + [action.actionName]: { + ...action, // actionName이 포함되는 문제가 있지만 있어도 상관은 없을 것 같습니다. + }, + }), + {} + ), + }, + features: features.concat([defaultActionLogFeature]), + }); + +module.exports = { + generateTarget, + recordAction, + buildResource, +}; diff --git a/src/modules/logger.js b/src/modules/logger.js index 0e02e56e..e532e7aa 100644 --- a/src/modules/logger.js +++ b/src/modules/logger.js @@ -1,72 +1,95 @@ -const { createLogger, format, transports } = require("winston"); -const dailyRotateFileTransport = require("winston-daily-rotate-file"); const path = require("path"); +const { createLogger, format, transports } = require("winston"); +const DailyRotateFileTransport = require("winston-daily-rotate-file"); const { nodeEnv } = require("../../loadenv"); -// 로깅에 사용하기 위한 포맷을 추가로 정의합니다. -const customFormat = { - time: "YYYY-MM-DD HH:mm:ss", // 로깅 시각 - line: format.printf(({ level, message, timestamp, stack }) => { - return `${timestamp} [${level}]: ${message} ${ - level === "error" ? stack : "" - }`; - }), // 메시지 포맷 - fileDate: "YYYY-MM-DD-HH", // 파일명에 포함되는 시각 -}; +// logger에서 사용할 포맷들을 정의합니다. +const baseFormat = format.combine( + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss(UTCZ)" }), + format.errors({ stack: true }), + format.splat(), + format.json() +); +const finalFormat = format.printf( + ({ level, message, timestamp, stack }) => + `${timestamp} [${level}]: ${message} ${ + level === "error" && stack !== undefined ? stack : "" + }` +); + +// 파일 출력 시 사용될 포맷. 색 관련 특수문자가 파일에 쓰여지는 것을 방지하기 위해 색상이 표시되지 않습니다. +const uncolorizedFormat = format.combine( + baseFormat, + format.uncolorize(), + finalFormat +); + +// 콘솔 출력 시 사용될 포맷. 색상이 표시됩니다. +const colorizedFormat = format.combine( + baseFormat, + format.colorize({ all: true }), + finalFormat +); + +// 로그 파일명에 포함되는 시각 +const datePattern = "YYYY-MM-DD-HH"; +// 로그 파일당 최대 크기(=5MB). +const maxSize = 5 * 1024 * 1024; + +// 콘솔에 출력하기 위한 winston transport +const consoleTransport = new transports.Console(); /** - * console.log 대신 사용되는 winston Logger 객체입니다. + * console.log()와 console.error() 대신 사용되는 winston Logger 객체입니다. * - * 전체 로그는 *.combined.log 파일에, 예외 처리로 핸들링 된 오류 로그는 *.error.log 파일에, 예외 처리가 되지 않은 오류는 *.unhandled.log에 저장됩니다. - * @property {function} info() - 일반적인 정보 기록을 위한 로깅(API 접근 기록 등)을 위해 사용합니다. - * @property {function} error() - 오류 메시지를 로깅하기 위해 사용합니다. + * - "production" 환경: 모든 로그는 파일 시스템에 저장되고, 콘솔로도 출력됩니다. + * - "development" & "test" 환경: 모든 로그는 콘솔에 출력됩니다. + * + * @method info(message: string, callback: winston.LogCallback) - 일반적인 정보(API 접근 등) 기록을 위해 사용합니다. + * @method error(message: string, callback: winston.LogCallback) - 오류 메시지를 기록하기 위해 사용합니다. */ -const logger = createLogger({ - format: format.combine( - format.timestamp({ format: customFormat.time }), - format.errors({ stack: true }), - format.splat(), - format.json(), - customFormat.line - ), - defaultMeta: { service: "sparcs-taxi" }, - transports: [ - new dailyRotateFileTransport({ - filename: path.resolve("logs/%DATE%-combined.log"), - datePattern: customFormat.fileDate, - maxsize: 5242880, // 5MB - level: "info", - }), - new dailyRotateFileTransport({ - filename: path.resolve("logs/%DATE%-error.log"), - datePattern: customFormat.fileDate, - maxsize: 5242880, // 5MB - level: "error", - }), - ], - exceptionHandlers: [ - new dailyRotateFileTransport({ - filename: path.resolve("logs/%DATE%-unhandled.log"), - datePattern: customFormat.fileDate, - maxsize: 5242880, // 5MB - }), - ], -}); - -// If the environment is not production, the log is also recorded on console -if (nodeEnv !== "production") { - logger.add( - new transports.Console({ - format: format.combine( - format.timestamp({ format: customFormat.time }), - format.errors({ stack: true }), - format.splat(), - format.colorize(), - customFormat.line - ), - }) - ); -} +const logger = + nodeEnv === "production" + ? // "production" 환경에서 사용되는 Logger 객체 + createLogger({ + level: "info", + format: uncolorizedFormat, + defaultMeta: { service: "sparcs-taxi" }, + transports: [ + // 전체 로그("info", "warn", "error")를 파일로 출력합니다. + new DailyRotateFileTransport({ + level: "info", + filename: path.resolve("logs/%DATE%-combined.log"), + datePattern, + maxSize, + }), + // 예외 처리로 핸들링 된 오류 로그("error")를 파일과 콘솔에 출력합니다. + new DailyRotateFileTransport({ + level: "error", + filename: path.resolve("logs/%DATE%-error.log"), + datePattern, + maxSize, + }), + consoleTransport, + ], + exceptionHandlers: [ + // 예외 처리가 되지 않은 오류 로그("error")를 파일과 콘솔에 출력합니다. + new DailyRotateFileTransport({ + filename: path.resolve("logs/%DATE%-unhandled.log"), + datePattern, + maxSize, + }), + consoleTransport, + ], + }) + : // "development", "test" 환경에서 사용되는 Logger 객체 + createLogger({ + level: "info", + format: colorizedFormat, + defaultMeta: { service: "sparcs-kaist" }, + transports: [consoleTransport], + exceptionHandlers: [consoleTransport], + }); module.exports = logger; diff --git a/src/modules/socket.js b/src/modules/socket.js index b2ad4966..1e04cfcd 100644 --- a/src/modules/socket.js +++ b/src/modules/socket.js @@ -44,9 +44,9 @@ const transformChatsForRoom = async (chats) => { chatsToSend.push({ roomId: chat.roomId, type: chat.type, - authorId: chat.authorId._id, - authorName: chat.authorId.nickname, - authorProfileUrl: chat.authorId.profileImageUrl, + authorId: chat.authorId?._id, + authorName: chat.authorId?.nickname, + authorProfileUrl: chat.authorId?.profileImageUrl, content: chat.content, time: chat.time, isValid: chat.isValid, @@ -61,36 +61,47 @@ const transformChatsForRoom = async (chats) => { * FCM 알림으로 보내는 content는 채팅 type에 따라 달라집니다. * 예를 들어, type이 "text"인 경우 `${nickname}: ${content}`를 보냅니다. */ -const getMessageBody = (type, nickname, content) => { +const getMessageBody = (type, nickname = "", content = "") => { + // 닉네임이 9글자를 넘어가면 "..."으로 표시합니다. + const ellipsisedNickname = + nickname.length > 9 ? nickname.slice(0, 7) + "..." : nickname; + // TODO: 채팅 메시지 유형에 따라 Body를 다르게 표시합니다. - if (type === "text") { - // 채팅 메시지 유형이 텍스트인 경우 본문은 "${nickname}: ${content}"가 됩니다. - return `${nickname}: ${content}`; - } else if (type === "s3img") { - // 채팅 유형이 이미지인 경우 본문은 "${nickname} 님이 이미지를 전송하였습니다"가 됩니다. - // TODO: 사용자 언어를 가져올 수 있으면 개선할 수 있다고 생각합니다. - const suffix = " 님이 이미지를 전송하였습니다"; - return `${nickname} ${suffix}`; - } else if (type === "in" || type === "out") { - // 채팅 메시지 type이 "in"이거나 "out"인 경우 본문은 "${nickname} 님이 입장하였습니다" 또는 "${nickname} 님이 퇴장하였습니다"가 됩니다. - // TODO: 사용자 언어를 가져올 수 있으면 개선할 수 있다고 생각합니다. - const suffix = - type === "in" ? " 님이 입장하였습니다" : "님이 퇴장하였습니다"; - return `${nickname} ${suffix}`; - } else if (type === "payment" || type === "settlement") { - // 채팅 메시지 type이 "in"이거나 "out"인 경우 본문은 "${nickname} 님이 결제를 완료하였습니다" 또는 "${nickname} 님이 정산을 완료하였습니다"가 됩니다. - // TODO: 사용자 언어를 가져올 수 있다면 개선할 수 있다고 생각합니다. - const suffix = - type === "payment" - ? " 님이 결제를 완료하였습니다" - : " 님이 정산을 완료하였습니다"; - return `${nickname} ${suffix}`; - } else if (type === "account") { - const suffix = " 님이 계좌번호를 전송하였습니다"; - return `${nickname} ${suffix}`; - } else { - // 정의되지 않은 type의 경우에는 nickname만 반환합니다. - return nickname; + // TODO: 사용자 언어를 가져올 수 있으면 개선할 수 있다고 생각합니다. + switch (type) { + case "text": + return `${ellipsisedNickname}: ${content}`; + case "s3img": { + const suffix = "님이 이미지를 전송하였습니다"; + return `${ellipsisedNickname} ${suffix}`; + } + case "in": { + const suffix = "님이 입장하였습니다"; + return `${ellipsisedNickname} ${suffix}`; + } + case "out": { + const suffix = "님이 퇴장하였습니다"; + return `${ellipsisedNickname} ${suffix}`; + } + case "payment": { + const suffix = "님이 정산을 시작하였습니다"; + return `${ellipsisedNickname} ${suffix}`; + } + case "settlement": { + const suffix = "님이 송금을 완료하였습니다"; + return `${ellipsisedNickname} ${suffix}`; + } + case type === "account": { + const suffix = "님이 계좌번호를 전송하였습니다"; + return `${ellipsisedNickname} ${suffix}`; + } + case "departure": + return `택시 출발 ${content}분 전 입니다`; + case "arrival": + return "아직 정산 시작을 하지 않았거나 송금을 완료하지 않은 사용자가 있습니다"; + default: + // 정의되지 않은 type의 경우에는 nickname만 반환합니다. + return ellipsisedNickname; } }; @@ -100,30 +111,35 @@ const getMessageBody = (type, nickname, content) => { * @param {Server} io - Socket.io 서버 인스턴스입니다. req.app.get("io")를 통해 접근할 수 있습니다. * @param {Object} chat - 채팅 메시지 내용입니다. * @param {string} chat.roomId - 채팅 및 채팅 알림을 보낼 방의 ObjectId입니다. - * @param {string} chat.type - 채팅 메시지의 유형입니다. "text" | "s3img" | "in" | "out" | "payment" | "settlement" 입니다. + * @param {string} chat.type - 채팅 메시지의 유형입니다. "text" | "s3img" | "in" | "out" | "payment" | "settlement" | "account" | "departure" | "arrival" 입니다. * @param {string} chat.content - 채팅 메시지의 본문입니다. chat.type이 "s3img"인 경우에는 채팅의 objectId입니다. chat.type이 "in"이거나 "out"인 경우 입퇴장한 사용자의 id(!==ObjectId)입니다. - * @param {string} chat.authorId - 채팅을 보낸 사용자의 ObjectId입니다. + * @param {string} chat.authorId - optional. 채팅을 보낸 사용자의 ObjectId입니다. * @param {Date?} chat.time - optional. 채팅 메시지 전송 시각입니다. * @return {Promise} 채팅 및 알림 전송에 성공하면 true, 중간에 오류가 발생하면 false를 반환합니다. */ const emitChatEvent = async (io, chat) => { try { - // chat must contain type, content and authorId - // chat can contain time or not. const { roomId, type, content, authorId } = chat; - if (!io || !roomId || !type || !content || !authorId) { + // chat must contains roomId, type, and content + if (!io || !roomId || !type || !content) { throw new IllegalArgumentsException(); } + // chat optionally contains time const time = chat?.time || Date.now(); + + // roomId must be valid const { name, part } = await roomModel.findById(roomId, "name part"); - const { nickname, profileImageUrl } = await userModel.findById( - authorId, - "nickname profileImageUrl" - ); + if (!name || !part) { + throw new IllegalArgumentsException(); + } - if (!nickname || !profileImageUrl || !name || !part) { + // chat optionally contains authorId + const { nickname, profileImageUrl } = authorId + ? await userModel.findById(authorId, "nickname profileImageUrl") + : {}; + if (authorId && (!nickname || !profileImageUrl)) { throw new IllegalArgumentsException(); } @@ -151,14 +167,12 @@ const emitChatEvent = async (io, chat) => { chatDocument.authorName = nickname; chatDocument.authorProfileUrl = profileImageUrl; - const urlOnClick = `/myroom/${roomId}`; const userIds = part.map((participant) => participant.user); - const userIdsExceptAuthor = part - .map((participant) => participant.user) - .filter((userId) => userId.toString() !== authorId.toString()); - const deviceTokens = await getTokensOfUsers(userIdsExceptAuthor, { - chatting: true, - }); + const userIdsExceptAuthor = authorId + ? part + .map((participant) => participant.user) + .filter((userId) => userId.toString() !== authorId.toString()) + : userIds; // 방의 모든 사용자에게 socket 메세지 수신 이벤트를 발생시킵니다. const chatsForRoom = await transformChatsForRoom([chatDocument]); @@ -171,15 +185,20 @@ const emitChatEvent = async (io, chat) => { ) ); - // 해당 방에 참여중인 사용자들에게 알림을 전송합니다. + // 방의 작성자를 제외한 참여중인 사용자들에게 푸시 알림을 전송합니다. + const deviceTokensExceptAuthor = await getTokensOfUsers( + userIdsExceptAuthor, + { chatting: true } + ); await sendMessageByTokens( - deviceTokens, + deviceTokensExceptAuthor, type, name, getMessageBody(type, nickname, content), getS3Url(`/profile-img/${profileImageUrl}`), - urlOnClick + `/myroom/${roomId}` ); + return true; } catch (err) { logger.error(err); diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index 53548965..7223e368 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -123,9 +123,19 @@ const chatSchema = Schema({ roomId: { type: Schema.Types.ObjectId, ref: "Room", required: true }, type: { type: String, - enum: ["text", "in", "out", "s3img", "payment", "settlement", "account"], + enum: [ + "text", + "in", + "out", + "s3img", + "payment", + "settlement", + "account", + "departure", // 출발 15분 전 알림 + "arrival", // 출발 (1|24)시간 이후 알림 - 정산/송금 권유 + ], }, // 메시지 종류 - authorId: { type: Schema.Types.ObjectId, ref: "User", required: true }, // 작성자 id + authorId: { type: Schema.Types.ObjectId, ref: "User" }, // 작성자 id content: { type: String, default: "" }, time: { type: Date, required: true }, isValid: { type: Boolean, default: true }, @@ -182,12 +192,14 @@ database.on("disconnected", function () { }, 5000); }); -mongoose.connect(mongoUrl, { - useNewUrlParser: true, - useUnifiedTopology: true, -}); +const connectDatabase = () => + mongoose.connect(mongoUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); module.exports = { + connectDatabase, userModel: mongoose.model("User", userSchema), deviceTokenModel: mongoose.model("DeviceToken", deviceTokenSchema), notificationOptionModel: mongoose.model( diff --git a/src/routes/admin.js b/src/routes/admin.js index 363e1282..d476142e 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -1,6 +1,5 @@ const express = require("express"); const AdminJS = require("adminjs"); -const { buildFeature } = require("adminjs"); const AdminJSExpress = require("@adminjs/express"); const AdminJSMongoose = require("@adminjs/mongoose"); const { @@ -14,6 +13,8 @@ const { deviceTokenModel, notificationOptionModel, } = require("../modules/stores/mongo"); +const { eventMode } = require("../../loadenv"); +const { buildResource } = require("../modules/adminResource"); const router = express.Router(); @@ -24,72 +25,23 @@ router.use(require("../middlewares/auth")); // Registration of the mongoose adapter AdminJS.registerAdapter(AdminJSMongoose); -// AdminJS에서 Log 저장을 하는 action -const logAction = (actionName) => async (res, req, context) => { - const user = await userModel.findOne({ id: req.userId }); - const modelName = context?.resource?.MongooseModel?.modelName; - const recordLength = `(length = ${context?.records?.length})`; - const recordId = `(_id = ${context?.record?.params?._id})` || recordLength; - const [action, target] = { - list: ["read", `List<${modelName}>${recordLength}`], - show: ["read", `${modelName}${recordId}`], - new: ["create", `${modelName}${recordId}`], - edit: ["update", `${modelName}${recordId}`], - delete: ["delete", `${modelName}${recordId}`], - bulkDelete: ["delete", `${modelName}${recordId}`], - }?.[actionName]; - - if ( - ["new", "edit", "bulkDelete"].includes(actionName) && - req.method !== "post" - ) - return res; - - if (user?._id && action && target) { - const newLog = new adminLogModel({ - user: user._id, // Log 취급자 User - time: req.timestamp, // Log 발생 시각 - ip: req.clientIP, // 접속 IP 주소 - target, // 처리한 정보주체 정보 - action, // 수행 업무 - }); - await newLog.save(); - } - return res; -}; - -// AdminJS에서 Log 기록을 하도록 action을 수정합니다 -const resourceWrapper = (resource) => ({ - resource, - features: [ - buildFeature({ - actions: ["list", "show", "new", "edit", "delete", "bulkDelete"].reduce( - (before, actionName) => ({ - ...before, - [actionName]: { - after: logAction(actionName), - }, - }), - {} - ), - }), - ], -}); +const baseResources = [ + userModel, + roomModel, + locationModel, + chatModel, + reportModel, + adminIPWhitelistModel, + adminLogModel, + deviceTokenModel, + notificationOptionModel, +].map(buildResource()); +const resources = baseResources.concat( + eventMode === "2023fall" ? require("../lottery").resources : [] +); // Create router for admin page -const adminJS = new AdminJS({ - resources: [ - userModel, - roomModel, - locationModel, - chatModel, - reportModel, - adminIPWhitelistModel, - adminLogModel, - deviceTokenModel, - notificationOptionModel, - ].map(resourceWrapper), -}); +const adminJS = new AdminJS({ resources }); router.use(AdminJSExpress.buildRouter(adminJS)); module.exports = router; diff --git a/src/routes/docs/logininfo.js b/src/routes/docs/logininfo.js index ec424f63..b24c5fe6 100644 --- a/src/routes/docs/logininfo.js +++ b/src/routes/docs/logininfo.js @@ -40,7 +40,7 @@ const logininfoDocs = { agreeOnTermsOfService: { type: "boolean", }, - subinfio: { + subinfo: { type: "object", properties: { kaist: { diff --git a/src/schedules/index.js b/src/schedules/index.js new file mode 100644 index 00000000..97818b92 --- /dev/null +++ b/src/schedules/index.js @@ -0,0 +1,8 @@ +const cron = require("node-cron"); + +const registerSchedules = (app) => { + cron.schedule("*/5 * * * *", require("./notifyBeforeDepart")(app)); + cron.schedule("*/10 * * * *", require("./notifyAfterArrival")(app)); +}; + +module.exports = registerSchedules; diff --git a/src/schedules/notifyAfterArrival.js b/src/schedules/notifyAfterArrival.js new file mode 100644 index 00000000..3202af08 --- /dev/null +++ b/src/schedules/notifyAfterArrival.js @@ -0,0 +1,59 @@ +const { roomModel, chatModel } = require("../modules/stores/mongo"); +// const { roomPopulateOption } = require("../modules/populates/rooms"); +const { emitChatEvent } = require("../modules/socket"); +const logger = require("../modules/logger"); + +const MS_PER_MINUTE = 60000; + +/** + * 출발 한시간 이후 정산/송금하기를 완료하지 않은 사용자가 있다면 알림을 전송합니다. + */ + +module.exports = (app) => async () => { + try { + const io = app.get("io"); + const expiredDate = new Date(Date.now() - 90 * MS_PER_MINUTE).toISOString(); + const arrivalDate = new Date(Date.now() - 60 * MS_PER_MINUTE).toISOString(); + + /** + * 알림을 전송하는 방의 첫 번째 조건은 다음과 같습니다. + * - 출발한 지 60분 이상 90분 이하가 지나야 합니다. + * - 결제를 진행한 사용자가 없거나, 결제가 진행된 후 정산을 하지 않은 사용자가 있어야 합니다. + */ + const candidateRooms = await roomModel.find({ + $and: [ + { time: { $gte: expiredDate } }, + { time: { $lte: arrivalDate } }, + { + part: { + $elemMatch: { settlementStatus: { $nin: ["paid", "sent"] } }, + }, + }, + ], + }); + + await Promise.all( + candidateRooms.map(async ({ _id: roomId, time }) => { + /** + * 알림을 전송하는 방의 두 번째 조건은 다음과 같습니다. + * - '출발 후 알림'이 아직 전송되지 않았어야 합니다. + * 모든 조건에 만족이 되면 해당 방에 참여 중인 모든 사용자에게 알림이 전송됩니다. + */ + const countArrivalChat = await chatModel.countDocuments({ + roomId, + type: "arrival", + }); + if (countArrivalChat > 0) return; + const minuteDiff = Math.floor((Date.now() - time) / MS_PER_MINUTE); + if (minuteDiff <= 0) return; + await emitChatEvent(io, { + roomId: roomId, + type: "arrival", + content: minuteDiff.toString(), + }); + }) + ); + } catch (err) { + logger.error(err); + } +}; diff --git a/src/schedules/notifyBeforeDepart.js b/src/schedules/notifyBeforeDepart.js new file mode 100644 index 00000000..ffe66386 --- /dev/null +++ b/src/schedules/notifyBeforeDepart.js @@ -0,0 +1,54 @@ +const { roomModel, chatModel } = require("../modules/stores/mongo"); +const { emitChatEvent } = require("../modules/socket"); +const logger = require("../modules/logger"); + +const MS_PER_MINUTE = 60000; + +/** + * 출발까지 15분 남은 방들에 참여하고 있는 사용자들에게 리마인더 알림을 전송합니다. + */ + +module.exports = (app) => async () => { + try { + const io = app.get("io"); + const currentDate = new Date(Date.now()).toISOString(); + const departDate = new Date(Date.now() + 15 * MS_PER_MINUTE).toISOString(); + + /** + * 알림을 전송하는 방의 첫 번째 조건은 다음과 같습니다. + * - 출출발까지 15분 이하가 남아있어야 합니다. + * - 2명 이상이 방에 참여 중이어야 합니다. + */ + const candidatesRooms = await roomModel.find({ + $and: [ + { time: { $gte: currentDate } }, + { time: { $lte: departDate } }, + { "part.1": { $exists: true } }, + ], + }); + + await Promise.all( + candidatesRooms.map(async ({ _id: roomId, time }) => { + /** + * 알림을 전송하는 방의 두 번째 조건은 다음과 같습니다. + * - '출발 후 알림'이 아직 전송되지 않았어야 합니다. + * 모든 조건에 만족이 되면 해당 방에 참여 중인 모든 사용자에게 알림이 전송됩니다. + */ + const countDepartureChat = await chatModel.countDocuments({ + roomId, + type: "departure", + }); + if (countDepartureChat > 0) return; + const minuteDiff = Math.ceil((time - Date.now()) / MS_PER_MINUTE); + if (minuteDiff <= 0) return; + await emitChatEvent(io, { + roomId: roomId, + type: "departure", + content: minuteDiff.toString(), + }); + }) + ); + } catch (err) { + logger.error(err); + } +}; diff --git a/src/services/auth.js b/src/services/auth.js index 1c62a817..bab3fc71 100644 --- a/src/services/auth.js +++ b/src/services/auth.js @@ -115,12 +115,14 @@ const sparcsssoHandler = (req, res) => { const sparcsssoCallbackHandler = (req, res) => { const loginAfterState = req.session?.loginAfterState; + const { state: stateForCmp, code } = req.query; + if (!loginAfterState) return res.status(400).send("SparcsssoCallbackHandler : invalid request"); - const { state, redirectOrigin, redirectPath } = loginAfterState; - const stateForCmp = req.body.state || req.query.state; + const { state, redirectOrigin, redirectPath } = loginAfterState; req.session.loginAfterState = undefined; + if (!state || !redirectOrigin || !redirectPath) { return res.status(400).send("SparcsssoCallbackHandler : invalid request"); } @@ -130,7 +132,6 @@ const sparcsssoCallbackHandler = (req, res) => { return res.redirect(redirectUrl); } - const code = req.body.code || req.query.code; client.getUserInfo(code).then((userDataBefore) => { const userData = transUserData(userDataBefore); const isTestAccount = testAccounts?.includes(userData.email); diff --git a/src/services/chats.js b/src/services/chats.js index 71017bdd..f26facde 100644 --- a/src/services/chats.js +++ b/src/services/chats.js @@ -1,14 +1,13 @@ const { chatModel, userModel, roomModel } = require("../modules/stores/mongo"); const { chatPopulateOption } = require("../modules/populates/chats"); +const { roomPopulateOption } = require("../modules/populates/rooms"); const aws = require("../modules/stores/aws"); const { transformChatsForRoom, emitChatEvent, emitUpdateEvent, } = require("../modules/socket"); -const { - roomPopulateOption, -} = require("../modules/populates/rooms"); +const logger = require("../modules/logger"); const chatCount = 60; @@ -49,6 +48,7 @@ const loadRecentChatHandler = async (req, res) => { res.status(500).send("Chat/ : internal server error"); } } catch (e) { + logger.error(e); res.status(500).send("Chat/ : internal server error"); } }; @@ -92,6 +92,7 @@ const loadBeforeChatHandler = async (req, res) => { res.status(500).send("Chat/load/before : internal server error"); } } catch (e) { + logger.error(e); res.status(500).send("Chat/load/before : internal server error"); } }; @@ -132,6 +133,7 @@ const loadAfterChatHandler = async (req, res) => { res.status(500).send("Chat/load/after : internal server error"); } } catch (e) { + logger.error(e); res.status(500).send("Chat/load/after : internal server error"); } }; @@ -168,6 +170,7 @@ const sendChatHandler = async (req, res) => { res.json({ result: true }); else res.status(500).send("Chat/send : internal server error"); } catch (e) { + logger.error(e); res.status(500).send("Chat/send : internal server error"); } }; @@ -262,6 +265,7 @@ const uploadChatImgGetPUrlHandler = async (req, res) => { }); }); } catch (e) { + logger.error(e); res.status(500).send("Chat/uploadChatImg/getPUrl : internal server error"); } }; @@ -308,6 +312,7 @@ const uploadChatImgDoneHandler = async (req, res) => { }); }); } catch (e) { + logger.error(e); res.status(500).send("Chat/uploadChatImg/done : internal server error"); } }; diff --git a/test/utils.js b/test/utils.js index 5fbfcd38..b3920e83 100644 --- a/test/utils.js +++ b/test/utils.js @@ -4,9 +4,12 @@ const { chatModel, locationModel, reportModel, + connectDatabase, } = require("../src/modules/stores/mongo"); const { generateProfileImageUrl } = require("../src/modules/modifyProfile"); +connectDatabase(); + // 테스트를 위한 유저 생성 함수 const userGenerator = async (username, testData) => { const testUser = new userModel({