diff --git a/azure-pipelines-staging.yml b/azure-pipelines-staging.yml index faef499..9782b91 100644 --- a/azure-pipelines-staging.yml +++ b/azure-pipelines-staging.yml @@ -83,6 +83,10 @@ jobs: REFRESH_TOKEN_LIFETIME_WEB: 0 REFRESH_TOKEN_LIFETIME_DEFAULT: 0 ACCESS_TOKEN_LIFETIME_WEB: 0 + REDIS_HOST: xxxxxxxxxxxx + REDIS_PORT: 0 + REDIS_PASSWORD: xxxxxxxxxxxx + ADB2C_CACHE_TTL: 0 - task: Docker@0 displayName: build inputs: diff --git a/dictation_server/.devcontainer/Dockerfile b/dictation_server/.devcontainer/Dockerfile index be42795..cd636c4 100644 --- a/dictation_server/.devcontainer/Dockerfile +++ b/dictation_server/.devcontainer/Dockerfile @@ -17,6 +17,11 @@ RUN bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "$ && apt-get install default-jre -y \ && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts +# Install redis-cli +RUN curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg +RUN sudo apt-get update -y +RUN sudo apt-get install redis -y + # COPY --from=golang:1.18-buster /usr/local/go/ /usr/local/go/ ENV GO111MODULE=auto COPY library-scripts/go-debian.sh /tmp/library-scripts/ diff --git a/dictation_server/.env.local.example b/dictation_server/.env.local.example index 3aa12ac..c59d570 100644 --- a/dictation_server/.env.local.example +++ b/dictation_server/.env.local.example @@ -29,4 +29,8 @@ STORAGE_ACCOUNT_ENDPOINT_EU=https://AAAAAAAAAAAAA ACCESS_TOKEN_LIFETIME_WEB=7200000 REFRESH_TOKEN_LIFETIME_WEB=86400000 REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000 -EMAIL_CONFIRM_LIFETIME=86400000 \ No newline at end of file +EMAIL_CONFIRM_LIFETIME=86400000 +REDIS_HOST=redis-cache +REDIS_PORT=6379 +REDIS_PASSWORD=omdsredispass +ADB2C_CACHE_TTL=86400 \ No newline at end of file diff --git a/dictation_server/package-lock.json b/dictation_server/package-lock.json index d9fffb4..da9ec43 100644 --- a/dictation_server/package-lock.json +++ b/dictation_server/package-lock.json @@ -26,7 +26,7 @@ "@types/jsonwebtoken": "^9.0.1", "@types/uuid": "^8.3.4", "axios": "^1.3.4", - "cache-manager": "^5.2.0", + "cache-manager": "^5.2.4", "cache-manager-redis-store": "^2.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -49,7 +49,8 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/swagger": "^6.3.0", "@nestjs/testing": "^9.3.12", - "@types/cache-manager-redis-store": "^2.0.1", + "@types/cache-manager": "^4.0.4", + "@types/cache-manager-redis-store": "^2.0.3", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.13", "@types/express-session": "^1.17.5", @@ -677,17 +678,89 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.0.tgz", @@ -737,12 +810,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.3.tgz", - "integrity": "sha512-QS3iR1GYC/YGUnW7IdggFeN5c1poPUurnGttOV/bZgPGV+izC/D8HnD6DLwod0fsatNyVn1G3EVWMYIF0nHbeA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.21.3", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -794,34 +867,34 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", - "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.20.7", - "@babel/types": "^7.21.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -880,30 +953,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -933,13 +1006,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -1018,9 +1091,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz", - "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1203,33 +1276,33 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.3.tgz", - "integrity": "sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.21.3", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.21.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.21.3", - "@babel/types": "^7.21.3", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1247,13 +1320,13 @@ } }, "node_modules/@babel/types": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.3.tgz", - "integrity": "sha512-sBGdETxC+/M4o/zKC0sl6sjWv62WFR/uzxrJ6uYyMLZOUlPnwzw0tKgVHOXxaAd5l2g8pEDM5RZ495GPQI77kg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2863,15 +2936,15 @@ } }, "node_modules/@types/cache-manager": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.2.tgz", - "integrity": "sha512-fT5FMdzsiSX0AbgnS5gDvHl2Nco0h5zYyjwDQy4yPC7Ww6DeGMVKPRqIZtg9HOXDV2kkc18SL1B0N8f0BecrCA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/cache-manager/-/cache-manager-4.0.4.tgz", + "integrity": "sha512-Kyk9uF54w5/JQWLDKr5378euWUPvebknZut6UpsKhO3R7vE5a5o71QxTR2uev1niBgVAoXAR+BCNMU1lipjxWQ==", "dev": true }, "node_modules/@types/cache-manager-redis-store": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/cache-manager-redis-store/-/cache-manager-redis-store-2.0.1.tgz", - "integrity": "sha512-8QuccvcPieh1xM/5kReE76SfdcIdEB0ePc+54ah/NBuK2eG+6O50SX4WKoJX81UxGdW3sh/WlDaDNqjnqxWNsA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/cache-manager-redis-store/-/cache-manager-redis-store-2.0.3.tgz", + "integrity": "sha512-6OpmRgz0KaTlh6zvqslxEKipCJWmTlI8HTtTzHkrYfPTpsISppaD2tRHaq6U+0jUCf1KvxMpm8RwCp2bnmFlZQ==", "dev": true, "dependencies": { "@types/cache-manager": "*", @@ -4264,12 +4337,12 @@ "optional": true }, "node_modules/cache-manager": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.2.3.tgz", - "integrity": "sha512-9OErI8fksFkxAMJ8Mco0aiZSdphyd90HcKiOMJQncSlU1yq/9lHHxrT8PDayxrmr9IIIZPOAEfXuGSD7g29uog==", + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.2.4.tgz", + "integrity": "sha512-gkuCjug16NdGvKm/sydxGVx17uffrSWcEe2xraBtwRCgdYcFxwJAla4OYpASAZT2yhSoxgDiWL9XH6IAChcZJA==", "dependencies": { "lodash.clonedeep": "^4.5.0", - "lru-cache": "^9.1.2" + "lru-cache": "^10.0.1" } }, "node_modules/cache-manager-redis-store": { @@ -4310,9 +4383,9 @@ } }, "node_modules/cache-manager/node_modules/lru-cache": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.2.tgz", - "integrity": "sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", "engines": { "node": "14 || >=16.14" } diff --git a/dictation_server/package.json b/dictation_server/package.json index 6523cee..415e65c 100644 --- a/dictation_server/package.json +++ b/dictation_server/package.json @@ -46,7 +46,7 @@ "@types/jsonwebtoken": "^9.0.1", "@types/uuid": "^8.3.4", "axios": "^1.3.4", - "cache-manager": "^5.2.0", + "cache-manager": "^5.2.4", "cache-manager-redis-store": "^2.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -69,7 +69,8 @@ "@nestjs/schematics": "^8.0.0", "@nestjs/swagger": "^6.3.0", "@nestjs/testing": "^9.3.12", - "@types/cache-manager-redis-store": "^2.0.1", + "@types/cache-manager": "^4.0.4", + "@types/cache-manager-redis-store": "^2.0.3", "@types/cookie-parser": "^1.4.3", "@types/express": "^4.17.13", "@types/express-session": "^1.17.5", diff --git a/dictation_server/redis.sh b/dictation_server/redis.sh new file mode 100644 index 0000000..91f36ca --- /dev/null +++ b/dictation_server/redis.sh @@ -0,0 +1,3 @@ +# source redis.sh で実行することでログインできる +source .env.local +redis-cli -h $REDIS_HOST -a $REDIS_PASSWORD \ No newline at end of file diff --git a/dictation_server/src/app.module.ts b/dictation_server/src/app.module.ts index f8c0664..667cde8 100644 --- a/dictation_server/src/app.module.ts +++ b/dictation_server/src/app.module.ts @@ -1,4 +1,4 @@ -import { MiddlewareConsumer, Module } from '@nestjs/common'; +import { CacheModule, MiddlewareConsumer, Module } from '@nestjs/common'; import { HealthController } from './health.controller'; import { ServeStaticModule } from '@nestjs/serve-static'; import { ConfigModule, ConfigService } from '@nestjs/config'; @@ -50,7 +50,8 @@ import { WorkflowsService } from './features/workflows/workflows.service'; import { validate } from './common/validators/env.validator'; import { WorkflowsRepositoryModule } from './repositories/workflows/workflows.repository.module'; import { TermsModule } from './features/terms/terms.module'; - +import { RedisModule } from './gateways/redis/redis.module'; +import * as redisStore from 'cache-manager-redis-store'; @Module({ imports: [ ServeStaticModule.forRootAsync({ @@ -103,6 +104,31 @@ import { TermsModule } from './features/terms/terms.module'; }), inject: [ConfigService], }), + CacheModule.registerAsync({ + imports: [ConfigModule], + useFactory: async (configService: ConfigService) => { + const host = configService.getOrThrow('REDIS_HOST'); + const port = configService.getOrThrow('REDIS_PORT'); + const password = configService.getOrThrow('REDIS_PASSWORD'); + if (process.env.STAGE === 'local') { + return { + store: redisStore, + host: host, + port: port, + password: password, + }; + } + + return { + store: redisStore, + url: `rediss://${host}:${port}`, + password: password, + tls: {}, + }; + }, + inject: [ConfigService], + isGlobal: true, + }), NotificationModule, NotificationhubModule, BlobstorageModule, @@ -110,6 +136,7 @@ import { TermsModule } from './features/terms/terms.module'; SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsModule, + RedisModule, ], controllers: [ HealthController, diff --git a/dictation_server/src/common/cache/constants.ts b/dictation_server/src/common/cache/constants.ts new file mode 100644 index 0000000..da6c13e --- /dev/null +++ b/dictation_server/src/common/cache/constants.ts @@ -0,0 +1 @@ +export const ADB2C_PREFIX = "adb2c-external-id:" \ No newline at end of file diff --git a/dictation_server/src/common/cache/index.ts b/dictation_server/src/common/cache/index.ts new file mode 100644 index 0000000..067be25 --- /dev/null +++ b/dictation_server/src/common/cache/index.ts @@ -0,0 +1,19 @@ +import { ADB2C_PREFIX } from './constants'; + +/** + * ADB2Cのユーザー格納用のキーを生成する + * @param externalId 外部ユーザーID + * @returns キャッシュのキー + */ +export const makeADB2CKey = (externalId: string): string => { + return `${ADB2C_PREFIX}${externalId}`; +} + +/** + * ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する + * @param key キャッシュのキー + * @returns 外部ユーザーID + */ +export const restoreAdB2cID = (key: string): string => { + return key.replace(ADB2C_PREFIX, ''); +} \ No newline at end of file diff --git a/dictation_server/src/common/test/modules.ts b/dictation_server/src/common/test/modules.ts index aec0d61..9c3d578 100644 --- a/dictation_server/src/common/test/modules.ts +++ b/dictation_server/src/common/test/modules.ts @@ -37,6 +37,7 @@ import { WorkflowsModule } from '../../features/workflows/workflows.module'; import { TermsService } from '../../features/terms/terms.service'; import { TermsRepositoryModule } from '../../repositories/terms/terms.repository.module'; import { TermsModule } from '../../features/terms/terms.module'; +import { CacheModule } from '@nestjs/common'; export const makeTestingModule = async ( datasource: DataSource, @@ -76,6 +77,7 @@ export const makeTestingModule = async ( SortCriteriaRepositoryModule, WorktypesRepositoryModule, TermsRepositoryModule, + CacheModule.register({ isGlobal: true }), ], providers: [ AuthService, diff --git a/dictation_server/src/common/token/index.ts b/dictation_server/src/common/token/index.ts index 415676a..dd97ce3 100644 --- a/dictation_server/src/common/token/index.ts +++ b/dictation_server/src/common/token/index.ts @@ -4,7 +4,6 @@ import type { IDToken, JwkSignKey, RefreshToken, - Aadb2cUser, } from './types'; import { isIDToken } from './typeguard'; @@ -14,6 +13,5 @@ export type { IDToken, JwkSignKey, RefreshToken, - Aadb2cUser, }; export { isIDToken }; diff --git a/dictation_server/src/common/token/types.ts b/dictation_server/src/common/token/types.ts index e1df6d9..b602913 100644 --- a/dictation_server/src/common/token/types.ts +++ b/dictation_server/src/common/token/types.ts @@ -56,8 +56,3 @@ export type JwkSignKey = { e: string; n: string; }; - -export type Aadb2cUser = { - displayName: string; - mail: string; -}; diff --git a/dictation_server/src/common/validators/env.validator.ts b/dictation_server/src/common/validators/env.validator.ts index 836de47..8ce706f 100644 --- a/dictation_server/src/common/validators/env.validator.ts +++ b/dictation_server/src/common/validators/env.validator.ts @@ -156,6 +156,22 @@ export class EnvValidator { @IsNotEmpty() @IsNumber() EMAIL_CONFIRM_LIFETIME: number; + + @IsNotEmpty() + @IsString() + REDIS_HOST: string; + + @IsNotEmpty() + @IsNumber() + REDIS_PORT: number; + + @IsNotEmpty() + @IsString() + REDIS_PASSWORD: string; + + @IsNotEmpty() + @IsNumber() + ADB2C_CACHE_TTL: number; } export function validate(config: Record) { diff --git a/dictation_server/src/features/users/test/users.service.mock.ts b/dictation_server/src/features/users/test/users.service.mock.ts index b862547..0c44550 100644 --- a/dictation_server/src/features/users/test/users.service.mock.ts +++ b/dictation_server/src/features/users/test/users.service.mock.ts @@ -1,6 +1,6 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../../common/token'; +import { B2cMetadata, JwkSignKey } from '../../../common/token'; import { AdB2cService, ConflictError, @@ -42,7 +42,7 @@ export type AdB2cMockValue = { getSignKeySets: JwkSignKey[] | Error; changePassword: { sub: string } | Error; createUser: string | ConflictError | Error; - getUser: Aadb2cUser | Error; + getUser: AdB2cUser | Error; getUsers: AdB2cUser[] | Error; }; @@ -184,10 +184,10 @@ export const makeAdB2cServiceMock = (value: AdB2cMockValue) => { getUser: getUser instanceof Error ? jest - .fn, []>() + .fn, []>() .mockRejectedValue(getUser) : jest - .fn, []>() + .fn, []>() .mockResolvedValue(getUser), getUsers: getUsers instanceof Error @@ -337,8 +337,8 @@ export const makeDefaultAdB2cMockValue = (): AdB2cMockValue => { }, createUser: '001', getUser: { + id: "xxxx-xxxxx-xxxxx-xxxx", displayName: 'Hanako Sato', - mail: 'hanako@sample.com', }, getUsers: AdB2cMockUsers, }; diff --git a/dictation_server/src/features/users/types/types.ts b/dictation_server/src/features/users/types/types.ts index b30b637..af38e72 100644 --- a/dictation_server/src/features/users/types/types.ts +++ b/dictation_server/src/features/users/types/types.ts @@ -10,7 +10,6 @@ import { IsPasswordvalid, } from '../../../common/validators/encryptionPassword.validator'; import { IsRoleAuthorDataValid } from '../../../common/validators/roleAuthor.validator'; -import { Aadb2cUser } from '../../../common/token'; export class ConfirmRequest { @ApiProperty() diff --git a/dictation_server/src/gateways/adb2c/adb2c.module.ts b/dictation_server/src/gateways/adb2c/adb2c.module.ts index 95d40fb..d16d471 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.module.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.module.ts @@ -1,9 +1,10 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { AdB2cService } from './adb2c.service'; +import { RedisModule } from '../redis/redis.module'; @Module({ - imports: [ConfigModule], + imports: [ConfigModule, RedisModule], exports: [AdB2cService], providers: [AdB2cService], }) diff --git a/dictation_server/src/gateways/adb2c/adb2c.service.ts b/dictation_server/src/gateways/adb2c/adb2c.service.ts index f38f5c5..6997ee9 100644 --- a/dictation_server/src/gateways/adb2c/adb2c.service.ts +++ b/dictation_server/src/gateways/adb2c/adb2c.service.ts @@ -1,14 +1,16 @@ import { ClientSecretCredential } from '@azure/identity'; import { Client } from '@microsoft/microsoft-graph-client'; import { TokenCredentialAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials'; -import { Injectable, Logger } from '@nestjs/common'; +import { CACHE_MANAGER, Inject, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; -import { Aadb2cUser, B2cMetadata, JwkSignKey } from '../../common/token'; +import { B2cMetadata, JwkSignKey } from '../../common/token'; import { AdB2cResponse, AdB2cUser } from './types/types'; import { isPromiseRejectedResult } from './utils/utils'; import { Context } from '../../common/log'; import { ADB2C_SIGN_IN_TYPE, MANUAL_RECOVERY_REQUIRED } from '../../constants'; +import { makeADB2CKey, restoreAdB2cID } from '../../common/cache'; +import { RedisService } from '../redis/redis.service'; export type ConflictError = { reason: 'email'; @@ -35,9 +37,14 @@ export class AdB2cService { this.configService.getOrThrow('TENANT_NAME'); private readonly flowName = this.configService.getOrThrow('SIGNIN_FLOW_NAME'); + private readonly ttl = + this.configService.getOrThrow('ADB2C_CACHE_TTL'); private graphClient: Client; - constructor(private readonly configService: ConfigService) { + constructor( + private readonly configService: ConfigService, + private readonly redisService: RedisService, + ) { // ADB2Cへの認証情報 const credential = new ClientSecretCredential( this.configService.getOrThrow('ADB2C_TENANT_ID'), @@ -176,11 +183,25 @@ export class AdB2cService { * @param externalId 外部ユーザーID * @returns ユーザ情報 */ - async getUser(externalId: string): Promise { + async getUser(externalId: string): Promise { this.logger.log(`[IN] ${this.getUser.name}`); try { - return await this.graphClient.api(`users/${externalId}`).get(); + const key = makeADB2CKey(externalId); + + // キャッシュ上に存在していれば、キャッシュから取得する + const cachedUser = await this.redisService.get(key); + if (cachedUser) { + this.logger.log(`[CACHE HIT] id: ${externalId}`); + return cachedUser; + } + + // キャッシュ上に存在していなければ、ADB2Cから取得してキャッシュに保存する + const user = await this.graphClient.api(`users/${externalId}`).get(); + await this.redisService.set(key, user, this.ttl); + this.logger.log(`[ADB2C GET] externalId: ${externalId}`); + + return user; } catch (e) { this.logger.error(e); throw e; @@ -203,12 +224,23 @@ export class AdB2cService { } | params: { externalIds:[${externalIds.join(',')}] };`, ); - /* - TODO [Task2002] 現状の実装だと1リクエストで最大15パラメータまでしか設定できないため、 - 別タスクでアカウント単位の検索用パラメータを用いて取得するように修正する。 - タスク 2002: B2Cからの名前取得をより低コストで行えるように修正する - */ - const chunkExternalIds = splitArrayInChunksOfFifteen(externalIds); + const keys = externalIds.map((externalId) => makeADB2CKey(externalId)); + const cache = await this.redisService.mget(keys); + + // キャッシュ上に存在していれば、キャッシュから取得する + const cachedUsers = cache.flatMap((x) => (x.value ? [x.value] : [])); + if (cachedUsers.length > 0) { + this.logger.log( + `[CACHE HIT] ids: ${cachedUsers.map((x) => x.id).join(',')}`, + ); + } + + // キャッシュ上に存在していなければ、ADB2Cから取得する + const queryExternalIds = cache + .filter((x) => x.value === null) + .map((x) => restoreAdB2cID(x.key)); + + const chunkExternalIds = splitArrayInChunksOfFifteen(queryExternalIds); try { const b2cUsers: AdB2cUser[] = []; @@ -221,9 +253,22 @@ export class AdB2cService { .get(); b2cUsers.push(...res.value); + + // 取得したユーザーをキャッシュに保存する + const users = res.value.map((user) => { + const key = makeADB2CKey(user.id); + return { + key: key, + value: user, + }; + }); + await this.redisService.mset(users, this.ttl); + this.logger.log( + `[ADB2C GET] externalIds: ${res.value?.map((x) => x.id).join(',')}`, + ); } - return b2cUsers; + return [...cachedUsers, ...b2cUsers]; } catch (e) { this.logger.error(e); const { statusCode } = e; @@ -249,6 +294,15 @@ export class AdB2cService { try { // https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example await this.graphClient.api(`users/${externalId}`).delete(); + this.logger.log(`[ADB2C DELETE] externalId: ${externalId}`); + + // キャッシュからも削除する + try { + await this.redisService.del(makeADB2CKey(externalId)); + } catch (e) { + // キャッシュからの削除に失敗しても、ADB2Cからの削除は成功しているため例外はスローしない + this.logger.error(`error=${e}`); + } } catch (e) { this.logger.error(`error=${e}`); throw e; @@ -270,18 +324,27 @@ export class AdB2cService { try { // 複数ユーザーを一括削除する方法がないため、1人ずつで削除を行う(rate limitに大きな影響がないこと確認済) const results = await Promise.allSettled( - externalIds.map( - async (x) => await this.graphClient.api(`users/${x}`).delete(), - ), - ); + externalIds.map(async (externalId) => { + await this.graphClient.api(`users/${externalId}`).delete(); + this.logger.log(`[ADB2C DELETE] externalId: ${externalId}`); - // 失敗したプロミスを抽出 - const failedPromises = results.filter( - (result) => result.status === 'rejected', + // キャッシュからも削除する + try { + await this.redisService.del(makeADB2CKey(externalId)); + } catch (e) { + // キャッシュからの削除に失敗しても、ADB2Cからの削除は成功しているため例外はスローしない + this.logger.error(`error=${e}`); + } + }), ); // 失敗したプロミスのエラーをログに記録 - failedPromises.forEach((result, index) => { + results.forEach((result, index) => { + // statusがrejectedでない場合は、エラーが発生していないためログに記録しない + if (result.status !== 'rejected') { + return; + } + const failedId = externalIds[index]; if (isPromiseRejectedResult(result)) { const error = result.reason.toString(); @@ -304,7 +367,8 @@ export class AdB2cService { } } -// TODO [Task2002] 文字列の配列を15要素ずつ区切る(この処理も別タスクで削除予定) +// ID指定で検索できる最大数は15のため、文字列の配列を15要素ずつ区切る。 +// PJ状況的にAzure AD B2C側のユーザーパラメータを増やして一括Queryできるようにする事が難しいので個別にQueryする。[2023/10/29] const splitArrayInChunksOfFifteen = (arr: string[]): string[][] => { const result: string[][] = []; const chunkSize = 15; // SDKの制限数 diff --git a/dictation_server/src/gateways/redis/redis.module.ts b/dictation_server/src/gateways/redis/redis.module.ts new file mode 100644 index 0000000..b019dec --- /dev/null +++ b/dictation_server/src/gateways/redis/redis.module.ts @@ -0,0 +1,8 @@ +import { CacheModule, Module } from '@nestjs/common'; +import { RedisService } from './redis.service'; +@Module({ + imports: [], + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/dictation_server/src/gateways/redis/redis.service.ts b/dictation_server/src/gateways/redis/redis.service.ts new file mode 100644 index 0000000..e932a19 --- /dev/null +++ b/dictation_server/src/gateways/redis/redis.service.ts @@ -0,0 +1,110 @@ +import { + CACHE_MANAGER, + Inject, + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; +import { Cache } from 'cache-manager'; + +@Injectable() +export class RedisService { + private readonly logger = new Logger(RedisService.name); + + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} + + /** + * キーに対応する値を設定する。 + * @param key キー + * @param value キーに対応する値 + * @param ttl 有効期限(秒) + */ + async set( + key: string, + value: unknown, + ttl?: number | undefined, + ): Promise { + try { + // cache-manager-redis-store がcache-managerのset形式と不一致な値の渡し方を採用しているため、 + // @types/cache-managerのset形式を使用すると、redisに値が保存されない。 + // そのため、{ttl : ttl} をany型として渡すことで、強引にcache-manager-redis-storeのsetを使用する。 + // https://www.npmjs.com/package/cache-manager + await this.cacheManager.set(key, value, { ttl: ttl } as any); + } catch (error) { + this.logger.error(error); + } + } + + /** + * 複数のキーとそのキーに対応する値を設定する。 + * @template T + * @param records キーとそのキーに対応する値のペアの配列 + * @param ttl 有効期限(秒) + */ + async mset( + records: { key: string; value: T }[], + ttl?: number | undefined, + ): Promise { + try { + // cache-manager-redis-store のmsetが壊れており、利用できないため、 + // 一つずつsetする。 + for await (const record of records) { + await this.set(record.key, record.value, ttl); + } + } catch (error) { + this.logger.error(error); + } + } + + /** + * キーに対応する値を取得する。 + * @template T + * @param key キー + * @returns キーに対応する値 + */ + async get(key: string): Promise { + try { + const value = await this.cacheManager.get(key); + return value; + } catch (error) { + this.logger.error(error); + return undefined; + } + } + + /** + * 複数のキーに対して、対応する値を取得する。 + * キーに対応する値がなかった場合、valueにはnullがセットされる。 + * @param keys キーの配列 + * @returns キーとそのキーに対応する値のペアの配列 + */ + async mget(keys: string[]): Promise<{ key: string; value: T | null }[]> { + if (keys.length === 0) return []; // mget操作は0件の時エラーとなるため、0件は特別扱いする + + try { + const records = await this.cacheManager.store.mget(...keys); + // getで取得した順序とKeysの順序は一致するはずなので、indexを利用してペアになるよう加工する + return records.map((record, index) => { + return { + key: keys[index], + value: record ? (record as T) : null, + }; + }); + } catch (error) { + this.logger.error(error); + return []; + } + } + + /** + * キーに対応する値を削除する。 + * @param key キー + */ + async del(key: string): Promise { + try { + await this.cacheManager.del(key); + } catch (error) { + this.logger.error(error); + } + } +}