Merge branch 'develop' into main
This commit is contained in:
commit
b00cffae4c
@ -80,9 +80,13 @@ jobs:
|
||||
TENANT_NAME: xxxxxxxxxxxx
|
||||
SIGNIN_FLOW_NAME: xxxxxxxxxxxx
|
||||
STORAGE_TOKEN_EXPIRE_TIME: 0
|
||||
REFRESH_TOKEN_LIFETIME_WEB: 0
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT: 0
|
||||
ACCESS_TOKEN_LIFETIME_WEB: 0
|
||||
REFRESH_TOKEN_LIFETIME_WEB: 86400000
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT: 2592000000
|
||||
ACCESS_TOKEN_LIFETIME_WEB: 7200000
|
||||
REDIS_HOST: xxxxxxxxxxxx
|
||||
REDIS_PORT: 0
|
||||
REDIS_PASSWORD: xxxxxxxxxxxx
|
||||
ADB2C_CACHE_TTL: 0
|
||||
- task: Docker@0
|
||||
displayName: build
|
||||
inputs:
|
||||
|
||||
@ -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/
|
||||
|
||||
@ -30,3 +30,7 @@ ACCESS_TOKEN_LIFETIME_WEB=7200000
|
||||
REFRESH_TOKEN_LIFETIME_WEB=86400000
|
||||
REFRESH_TOKEN_LIFETIME_DEFAULT=2592000000
|
||||
EMAIL_CONFIRM_LIFETIME=86400000
|
||||
REDIS_HOST=redis-cache
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=omdsredispass
|
||||
ADB2C_CACHE_TTL=86400
|
||||
223
dictation_server/package-lock.json
generated
223
dictation_server/package-lock.json
generated
@ -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"
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
3
dictation_server/redis.sh
Normal file
3
dictation_server/redis.sh
Normal file
@ -0,0 +1,3 @@
|
||||
# source redis.sh で実行することでログインできる
|
||||
source .env.local
|
||||
redis-cli -h $REDIS_HOST -a $REDIS_PASSWORD
|
||||
@ -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<string>('REDIS_HOST');
|
||||
const port = configService.getOrThrow<number>('REDIS_PORT');
|
||||
const password = configService.getOrThrow<string>('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,
|
||||
|
||||
1
dictation_server/src/common/cache/constants.ts
vendored
Normal file
1
dictation_server/src/common/cache/constants.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export const ADB2C_PREFIX = "adb2c-external-id:"
|
||||
19
dictation_server/src/common/cache/index.ts
vendored
Normal file
19
dictation_server/src/common/cache/index.ts
vendored
Normal file
@ -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, '');
|
||||
}
|
||||
@ -38,6 +38,7 @@ export const ErrorCodes = [
|
||||
'E010401', // PONumber重複エラー
|
||||
'E010501', // アカウント不在エラー
|
||||
'E010502', // アカウント情報変更不可エラー
|
||||
'E010503', // 代行操作不許可エラー
|
||||
'E010601', // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
'E010602', // タスク変更権限不足エラー
|
||||
'E010603', // タスク不在エラー
|
||||
|
||||
@ -27,6 +27,7 @@ export const errors: Errors = {
|
||||
E010401: 'This PoNumber already used Error',
|
||||
E010501: 'Account not Found Error.',
|
||||
E010502: 'Account information cannot be changed Error.',
|
||||
E010503: 'Delegation not allowed Error.',
|
||||
E010601: 'Task is not Editable Error',
|
||||
E010602: 'No task edit permissions Error',
|
||||
E010603: 'Task not found Error.',
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { Context } from './types';
|
||||
|
||||
export const makeContext = (externalId: string): Context => {
|
||||
export const makeContext = (
|
||||
externalId: string,
|
||||
delegationId?: string,
|
||||
): Context => {
|
||||
return {
|
||||
trackingId: externalId,
|
||||
delegationId: delegationId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -3,4 +3,8 @@ export class Context {
|
||||
* APIの操作ユーザーを追跡するためのID
|
||||
*/
|
||||
trackingId: string;
|
||||
/**
|
||||
* APIの代行操作ユーザーを追跡するためのID
|
||||
*/
|
||||
delegationId?: string | undefined;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
export type RefreshToken = {
|
||||
/**
|
||||
* 外部認証サービスの識別子(代行者)
|
||||
*/
|
||||
delegateUserId?: string | undefined;
|
||||
/**
|
||||
* 外部認証サービスの識別子
|
||||
*/
|
||||
@ -14,6 +18,10 @@ export type RefreshToken = {
|
||||
};
|
||||
|
||||
export type AccessToken = {
|
||||
/**
|
||||
* 外部認証サービスの識別子(代行者)
|
||||
*/
|
||||
delegateUserId?: string | undefined;
|
||||
/**
|
||||
* 外部認証サービスの識別子
|
||||
*/
|
||||
@ -48,8 +56,3 @@ export type JwkSignKey = {
|
||||
e: string;
|
||||
n: string;
|
||||
};
|
||||
|
||||
export type Aadb2cUser = {
|
||||
displayName: string;
|
||||
mail: string;
|
||||
};
|
||||
|
||||
@ -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<string, unknown>) {
|
||||
|
||||
@ -14,7 +14,6 @@ import { UserGroup } from '../../../repositories/user_groups/entity/user_group.e
|
||||
import { UserGroupsRepositoryService } from '../../../repositories/user_groups/user_groups.repository.service';
|
||||
import { AdB2cUser } from '../../../gateways/adb2c/types/types';
|
||||
import { LicensesRepositoryService } from '../../../repositories/licenses/licenses.repository.service';
|
||||
import { Context } from '../../../common/log';
|
||||
import { BlobstorageService } from '../../../gateways/blobstorage/blobstorage.service';
|
||||
import { Worktype } from '../../../repositories/worktypes/entity/worktype.entity';
|
||||
import { WorktypesRepositoryService } from '../../../repositories/worktypes/worktypes.repository.service';
|
||||
|
||||
@ -31,6 +31,8 @@ import { Request } from 'express';
|
||||
import { AuthGuard } from '../../common/guards/auth/authguards';
|
||||
import { RoleGuard } from '../../common/guards/role/roleguards';
|
||||
import { ADMIN_ROLES, TIERS } from '../../constants';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { AccessToken, RefreshToken } from '../../common/token';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
@ -183,18 +185,35 @@ export class AuthController {
|
||||
@Body() body: DelegationTokenRequest,
|
||||
): Promise<DelegationTokenResponse> {
|
||||
const { delegatedAccountId } = body;
|
||||
const refreshToken = retrieveAuthorizationToken(req);
|
||||
const token = retrieveAuthorizationToken(req);
|
||||
|
||||
if (!refreshToken) {
|
||||
if (!token) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(token, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
|
||||
const context = makeContext(uuidv4());
|
||||
const context = makeContext(userId);
|
||||
const refreshToken = await this.authService.generateDelegationRefreshToken(
|
||||
context,
|
||||
userId,
|
||||
delegatedAccountId,
|
||||
);
|
||||
const accessToken = await this.authService.generateDelegationAccessToken(
|
||||
context,
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
return { accessToken: '', refreshToken: '' };
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
@Post('delegation/access-token')
|
||||
@ -229,9 +248,23 @@ export class AuthController {
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const decodedRefreshToken = jwt.decode(refreshToken, { json: true });
|
||||
if (!decodedRefreshToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId, delegateUserId } = decodedRefreshToken as RefreshToken;
|
||||
|
||||
const context = makeContext(uuidv4());
|
||||
const context = makeContext(userId);
|
||||
const accessToken = await this.authService.updateDelegationAccessToken(
|
||||
context,
|
||||
delegateUserId,
|
||||
userId,
|
||||
refreshToken,
|
||||
);
|
||||
|
||||
return { accessToken: '' };
|
||||
return { accessToken };
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,10 +9,17 @@ import {
|
||||
import { DataSource } from 'typeorm';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { makeTestAccount } from '../../common/test/utility';
|
||||
import { getAccount, makeTestAccount } from '../../common/test/utility';
|
||||
import { AuthService } from './auth.service';
|
||||
import { createTermInfo } from './test/utility';
|
||||
import {
|
||||
createTermInfo,
|
||||
deleteAccount,
|
||||
updateAccountDelegationPermission,
|
||||
} from './test/utility';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { TIERS, USER_ROLES } from '../../constants';
|
||||
import { decode, isVerifyError } from '../../common/jwt';
|
||||
import { RefreshToken, AccessToken } from '../../common/token';
|
||||
|
||||
describe('AuthService', () => {
|
||||
it('IDトークンの検証とペイロードの取得に成功する', async () => {
|
||||
@ -276,6 +283,447 @@ describe('checkIsAcceptedLatestVersion', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateDelegationRefreshToken', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
it('代行操作が許可されたパートナーの代行操作用リフレッシュトークンを取得できること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeToken.role).toBe('none admin');
|
||||
expect(decodeToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
});
|
||||
it('代行操作が許可されていない場合、400エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { account: partnerAccount } = await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: false,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
try {
|
||||
await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010503'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('代行操作対象が存在しない場合、400エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: false,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
try {
|
||||
await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
9999,
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010501'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateDelegationAccessToken', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
it('代行操作用リフレッシュトークンから代行操作用アクセストークンを取得できること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
const delegationAccessToken = await service.generateDelegationAccessToken(
|
||||
context,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用アクセストークンをデコード
|
||||
const decodeAccessToken = decode<AccessToken>(delegationAccessToken);
|
||||
if (isVerifyError(decodeAccessToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeAccessToken.role).toBe('none admin');
|
||||
expect(decodeAccessToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeAccessToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeAccessToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
});
|
||||
|
||||
it('代行操作用リフレッシュトークンの形式が不正な場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin } = await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
try {
|
||||
await service.generateDelegationAccessToken(context, 'invalid token');
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.UNAUTHORIZED);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E000101'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDelegationAccessToken', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('代行操作用リフレッシュトークンから代行操作用アクセストークンを更新できること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
const token = await service.updateDelegationAccessToken(
|
||||
context,
|
||||
decodeRefreshToken.delegateUserId,
|
||||
decodeRefreshToken.userId,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeAccessToken = decode<RefreshToken>(token);
|
||||
if (isVerifyError(decodeAccessToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeAccessToken.role).toBe('none admin');
|
||||
expect(decodeAccessToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeAccessToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeAccessToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
});
|
||||
|
||||
it('代行操作対象アカウントの代行操作が許可されていない場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
if (decodeRefreshToken.delegateUserId === undefined) {
|
||||
fail();
|
||||
}
|
||||
|
||||
// 代行操作対象アカウントの代行操作を許可しないように変更
|
||||
await updateAccountDelegationPermission(source, partnerAccount.id, false);
|
||||
const account = await getAccount(source, partnerAccount.id);
|
||||
|
||||
expect(account?.delegation_permission ?? true).toBeFalsy();
|
||||
|
||||
try {
|
||||
await service.updateDelegationAccessToken(
|
||||
context,
|
||||
decodeRefreshToken.delegateUserId,
|
||||
decodeRefreshToken.userId,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.UNAUTHORIZED);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010503'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
it('代行操作対象アカウントが存在しない場合、エラーとなること', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
const service = module.get<AuthService>(AuthService);
|
||||
const { admin: parentAdmin, account: parentAccount } =
|
||||
await makeTestAccount(source, {
|
||||
tier: 4,
|
||||
});
|
||||
const { admin: partnerAdmin, account: partnerAccount } =
|
||||
await makeTestAccount(
|
||||
source,
|
||||
{
|
||||
tier: 5,
|
||||
parent_account_id: parentAccount.id,
|
||||
delegation_permission: true,
|
||||
},
|
||||
{ role: USER_ROLES.NONE },
|
||||
);
|
||||
|
||||
const context = makeContext(parentAdmin.external_id);
|
||||
|
||||
const delegationRefreshToken = await service.generateDelegationRefreshToken(
|
||||
context,
|
||||
parentAdmin.external_id,
|
||||
partnerAccount.id,
|
||||
);
|
||||
|
||||
// 取得できた代行操作用リフレッシュトークンをデコード
|
||||
const decodeRefreshToken = decode<RefreshToken>(delegationRefreshToken);
|
||||
if (isVerifyError(decodeRefreshToken)) {
|
||||
fail();
|
||||
}
|
||||
|
||||
expect(decodeRefreshToken.role).toBe('none admin');
|
||||
expect(decodeRefreshToken.tier).toBe(TIERS.TIER5);
|
||||
expect(decodeRefreshToken.userId).toBe(partnerAdmin.external_id);
|
||||
expect(decodeRefreshToken.delegateUserId).toBe(parentAdmin.external_id);
|
||||
|
||||
if (decodeRefreshToken.delegateUserId === undefined) {
|
||||
fail();
|
||||
}
|
||||
|
||||
// 代行操作対象アカウントを削除
|
||||
deleteAccount(source, partnerAccount.id);
|
||||
|
||||
try {
|
||||
await service.updateDelegationAccessToken(
|
||||
context,
|
||||
decodeRefreshToken.delegateUserId,
|
||||
partnerAdmin.external_id,
|
||||
delegationRefreshToken,
|
||||
);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.UNAUTHORIZED);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010501'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const idTokenPayload = {
|
||||
exp: 9000000000,
|
||||
nbf: 1000000000,
|
||||
|
||||
@ -19,9 +19,21 @@ import {
|
||||
} from '../../common/token';
|
||||
import { ADMIN_ROLES, TIERS, USER_ROLES } from '../../constants';
|
||||
import { AdB2cService } from '../../gateways/adb2c/adb2c.service';
|
||||
import { User } from '../../repositories/users/entity/user.entity';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { Context } from '../../common/log';
|
||||
import {
|
||||
AccountNotFoundError,
|
||||
AdminUserNotFoundError,
|
||||
} from '../../repositories/accounts/errors/types';
|
||||
import {
|
||||
DelegationNotAllowedError,
|
||||
UserNotFoundError,
|
||||
} from '../../repositories/users/errors/types';
|
||||
import {
|
||||
InvalidTokenFormatError,
|
||||
RoleUnexpectedError,
|
||||
TierUnexpectedError,
|
||||
} from './errors/types';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -76,39 +88,23 @@ export class AuthService {
|
||||
`[IN] [${context.trackingId}] ${this.generateRefreshToken.name}`,
|
||||
);
|
||||
|
||||
let user: User;
|
||||
// ユーザー情報とユーザーが属しているアカウント情報を取得
|
||||
try {
|
||||
user = await this.usersRepository.findUserByExternalId(idToken.sub);
|
||||
const user = await this.usersRepository.findUserByExternalId(idToken.sub);
|
||||
if (!user.account) {
|
||||
throw new Error('Account information not found');
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// Tierのチェック
|
||||
const minTier = 1;
|
||||
const maxTier = 5;
|
||||
const userTier = user.account.tier;
|
||||
if (userTier < minTier || userTier > maxTier) {
|
||||
this.logger.error(
|
||||
throw new TierUnexpectedError(
|
||||
`Tier from DB is unexpected value. tier=${user.account.tier}`,
|
||||
);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010206'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
// 要求された環境用トークンの寿命を決定
|
||||
const refreshTokenLifetime =
|
||||
type === 'web'
|
||||
@ -117,26 +113,7 @@ export class AuthService {
|
||||
const privateKey = getPrivateKey(this.configService);
|
||||
|
||||
// ユーザーのロールを設定
|
||||
// 万一不正なRoleが登録されていた場合、そのままDBの値を使用すると不正なロールのリフレッシュトークンが発行されるため、
|
||||
// ロールの設定値はDBに保存したRoleの値を直接トークンに入れないように定数で設定する
|
||||
// ※none/author/typist以外はロールに設定されない
|
||||
let role = '';
|
||||
if (user.role === USER_ROLES.NONE) {
|
||||
role = USER_ROLES.NONE;
|
||||
} else if (user.role === USER_ROLES.AUTHOR) {
|
||||
role = USER_ROLES.AUTHOR;
|
||||
} else if (user.role === USER_ROLES.TYPIST) {
|
||||
role = USER_ROLES.TYPIST;
|
||||
} else {
|
||||
this.logger.error(`Role from DB is unexpected value. role=${user.role}`);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010205'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
const role = this.getUserRole(user.role);
|
||||
|
||||
const token = sign<RefreshToken>(
|
||||
{
|
||||
@ -154,10 +131,34 @@ export class AuthService {
|
||||
privateKey,
|
||||
);
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case TierUnexpectedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010206'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
case RoleUnexpectedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010205'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateRefreshToken.name}`,
|
||||
);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,6 +204,247 @@ export class AuthService {
|
||||
);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作用のリフレッシュトークンを生成します
|
||||
* @param context
|
||||
* @param delegateUserExternalId 代行操作者の外部認証サービスの識別子
|
||||
* @param originAccountId 代行操作対象アカウントのID
|
||||
* @returns delegation refresh token
|
||||
*/
|
||||
async generateDelegationRefreshToken(
|
||||
context: Context,
|
||||
delegateUserExternalId: string,
|
||||
originAccountId: number,
|
||||
): Promise<string> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.generateDelegationRefreshToken.name} | params: { ` +
|
||||
`delegateUserExternalId: ${delegateUserExternalId}, ` +
|
||||
`originAccountId: ${originAccountId}, };`,
|
||||
);
|
||||
|
||||
// ユーザー情報とユーザーが属しているアカウント情報を取得
|
||||
try {
|
||||
const user = await this.usersRepository.findUserByExternalId(
|
||||
delegateUserExternalId,
|
||||
);
|
||||
|
||||
// 代行操作対象アカウントの管理者ユーザーを取得
|
||||
const adminUser = await this.usersRepository.findDelegateUser(
|
||||
user.account_id,
|
||||
originAccountId,
|
||||
);
|
||||
|
||||
// 要求された環境用トークンの寿命を決定
|
||||
const refreshTokenLifetime = this.refreshTokenLifetimeWeb;
|
||||
const privateKey = getPrivateKey(this.configService);
|
||||
|
||||
// ユーザーのロールを設定
|
||||
const role = this.getUserRole(adminUser.role);
|
||||
|
||||
const token = sign<RefreshToken>(
|
||||
{
|
||||
role: `${role} ${ADMIN_ROLES.ADMIN}`,
|
||||
tier: TIERS.TIER5,
|
||||
userId: adminUser.external_id,
|
||||
delegateUserId: delegateUserExternalId,
|
||||
},
|
||||
refreshTokenLifetime,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
return token;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case AccountNotFoundError:
|
||||
case AdminUserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010501'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case DelegationNotAllowedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010503'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
case RoleUnexpectedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010205'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateDelegationRefreshToken.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作アクセストークンの更新
|
||||
* @param context
|
||||
* @param refreshToken
|
||||
* @returns delegation access token
|
||||
*/
|
||||
async generateDelegationAccessToken(
|
||||
context: Context,
|
||||
refreshToken: string,
|
||||
): Promise<string> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.generateDelegationAccessToken.name}`,
|
||||
);
|
||||
|
||||
const privateKey = getPrivateKey(this.configService);
|
||||
const pubkey = getPublicKey(this.configService);
|
||||
|
||||
const token = verify<RefreshToken>(refreshToken, pubkey);
|
||||
if (isVerifyError(token)) {
|
||||
this.logger.error(`${token.reason} | ${token.message}`);
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateDelegationAccessToken.name}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = sign<AccessToken>(
|
||||
{
|
||||
role: token.role,
|
||||
tier: token.tier,
|
||||
userId: token.userId,
|
||||
delegateUserId: token.delegateUserId,
|
||||
},
|
||||
this.accessTokenlifetime,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.generateDelegationAccessToken.name}`,
|
||||
);
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作用アクセストークンを更新する
|
||||
* @param context
|
||||
* @param delegateUserExternalId
|
||||
* @param originUserExternalId
|
||||
* @param refreshToken
|
||||
* @returns delegation access token
|
||||
*/
|
||||
async updateDelegationAccessToken(
|
||||
context: Context,
|
||||
delegateUserExternalId: string | undefined,
|
||||
originUserExternalId: string,
|
||||
refreshToken: string,
|
||||
): Promise<string> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.updateDelegationAccessToken.name} | params: { ` +
|
||||
`delegateUserExternalId: ${delegateUserExternalId}, ` +
|
||||
`originUserExternalId: ${originUserExternalId}, };`,
|
||||
);
|
||||
try {
|
||||
if (!delegateUserExternalId) {
|
||||
throw new UserNotFoundError('delegateUserExternalId is undefined');
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findUserByExternalId(
|
||||
delegateUserExternalId,
|
||||
);
|
||||
|
||||
const privateKey = getPrivateKey(this.configService);
|
||||
const pubkey = getPublicKey(this.configService);
|
||||
|
||||
// トークンの検証
|
||||
const decodedToken = verify<RefreshToken>(refreshToken, pubkey);
|
||||
if (isVerifyError(decodedToken)) {
|
||||
throw new InvalidTokenFormatError(
|
||||
`Invalid token format. ${decodedToken.reason} | ${decodedToken.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// トークンの生成には検証済みトークンの値を使用する
|
||||
const { userId, delegateUserId, tier, role } = decodedToken;
|
||||
|
||||
if (delegateUserId === undefined) {
|
||||
throw new AdminUserNotFoundError('delegateUserId is undefined');
|
||||
}
|
||||
|
||||
// 代行操作対象アカウントの管理者ユーザーが存在して、アカウントに対して代行操作権限があるか確認
|
||||
const delegationPermission =
|
||||
await this.usersRepository.isAllowDelegationPermission(
|
||||
user.account_id,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (!delegationPermission) {
|
||||
throw new DelegationNotAllowedError(
|
||||
`Delegation is not allowed. delegateUserId=${delegateUserId}, userId=${userId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = sign<AccessToken>(
|
||||
{
|
||||
role: role,
|
||||
tier: tier,
|
||||
userId: userId,
|
||||
delegateUserId: delegateUserId,
|
||||
},
|
||||
this.accessTokenlifetime,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
return accessToken;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof HttpException) {
|
||||
throw e;
|
||||
}
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case InvalidTokenFormatError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
case UserNotFoundError:
|
||||
case AccountNotFoundError:
|
||||
case AdminUserNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010501'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
case DelegationNotAllowedError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010503'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(
|
||||
`[OUT] [${context.trackingId}] ${this.updateDelegationAccessToken.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets id token
|
||||
* @param token
|
||||
@ -340,6 +582,26 @@ export class AuthService {
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
};
|
||||
/**
|
||||
* トークンに設定するユーザーのロールを取得
|
||||
*/
|
||||
getUserRole = (role: string): string => {
|
||||
// ユーザーのロールを設定
|
||||
// 万一不正なRoleが登録されていた場合、そのままDBの値を使用すると不正なロールのリフレッシュトークンが発行されるため、
|
||||
// ロールの設定値はDBに保存したRoleの値を直接トークンに入れないように定数で設定する
|
||||
// ※none/author/typist以外はロールに設定されない
|
||||
if (role === USER_ROLES.NONE) {
|
||||
return USER_ROLES.NONE;
|
||||
} else if (role === USER_ROLES.AUTHOR) {
|
||||
return USER_ROLES.AUTHOR;
|
||||
} else if (role === USER_ROLES.TYPIST) {
|
||||
return USER_ROLES.TYPIST;
|
||||
} else {
|
||||
throw new RoleUnexpectedError(
|
||||
`Role from DB is unexpected value. role=${role}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 同意済み利用規約バージョンが最新かチェック
|
||||
|
||||
6
dictation_server/src/features/auth/errors/types.ts
Normal file
6
dictation_server/src/features/auth/errors/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// Role文字列想定外エラー
|
||||
export class RoleUnexpectedError extends Error {}
|
||||
// Tier範囲想定外エラー
|
||||
export class TierUnexpectedError extends Error {}
|
||||
// トークン形式不正エラー
|
||||
export class InvalidTokenFormatError extends Error {}
|
||||
@ -1,5 +1,7 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { Term } from '../../../repositories/terms/entity/term.entity';
|
||||
import { Account } from '../../../repositories/accounts/entity/account.entity';
|
||||
import { User } from '../../../repositories/users/entity/user.entity';
|
||||
|
||||
export const createTermInfo = async (
|
||||
datasource: DataSource,
|
||||
@ -16,3 +18,21 @@ export const createTermInfo = async (
|
||||
});
|
||||
identifiers.pop() as Term;
|
||||
};
|
||||
|
||||
export const updateAccountDelegationPermission = async (
|
||||
dataSource: DataSource,
|
||||
id: number,
|
||||
delegationPermission: boolean,
|
||||
): Promise<void> => {
|
||||
await dataSource
|
||||
.getRepository(Account)
|
||||
.update({ id: id }, { delegation_permission: delegationPermission });
|
||||
};
|
||||
|
||||
export const deleteAccount = async (
|
||||
dataSource: DataSource,
|
||||
id: number,
|
||||
): Promise<void> => {
|
||||
await dataSource.getRepository(User).delete({ account_id: id });
|
||||
await dataSource.getRepository(Account).delete({ id: id });
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsInt } from 'class-validator';
|
||||
|
||||
export class TokenRequest {
|
||||
@ApiProperty()
|
||||
@ -28,6 +29,7 @@ export type TermsCheckInfo = {
|
||||
|
||||
export class DelegationTokenRequest {
|
||||
@ApiProperty({ description: '代行操作対象のアカウントID' })
|
||||
@IsInt()
|
||||
delegatedAccountId: number;
|
||||
}
|
||||
export class DelegationTokenResponse {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common';
|
||||
import { makeErrorResponse } from '../../common/error/makeErrorResponse';
|
||||
import { AccessToken } from '../../common/token';
|
||||
import { UsersRepositoryService } from '../../repositories/users/users.repository.service';
|
||||
import { TasksRepositoryService } from '../../repositories/tasks/tasks.repository.service';
|
||||
import { BlobstorageService } from '../../gateways/blobstorage/blobstorage.service';
|
||||
|
||||
@ -153,11 +153,41 @@ export class TasksController {
|
||||
'指定した文字起こしタスクの次のタスクに紐づく音声ファイルIDを取得します',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(
|
||||
RoleGuard.requireds({
|
||||
roles: [USER_ROLES.TYPIST],
|
||||
}),
|
||||
)
|
||||
async getNextAudioFile(
|
||||
@Headers() headers,
|
||||
@Query() body: AudioNextRequest,
|
||||
@Req() req: Request,
|
||||
@Query() param: AudioNextRequest,
|
||||
): Promise<AudioNextResponse> {
|
||||
return { nextFileId: 1234 };
|
||||
const { endedFileId } = param;
|
||||
|
||||
const accessToken = retrieveAuthorizationToken(req) as string;
|
||||
if (!accessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000107'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const decodedAccessToken = jwt.decode(accessToken, { json: true });
|
||||
if (!decodedAccessToken) {
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E000101'),
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
const { userId } = decodedAccessToken as AccessToken;
|
||||
const context = makeContext(userId);
|
||||
|
||||
const nextFileId = await this.taskService.getNextTask(
|
||||
context,
|
||||
userId,
|
||||
endedFileId,
|
||||
);
|
||||
|
||||
return { nextFileId };
|
||||
}
|
||||
|
||||
@Post(':audioFileId/checkout')
|
||||
|
||||
@ -20,8 +20,14 @@ import {
|
||||
} from './test/utility';
|
||||
import { Adb2cTooManyRequestsError } from '../../gateways/adb2c/adb2c.service';
|
||||
import { makeContext } from '../../common/log';
|
||||
import { makeTestSimpleAccount, makeTestUser } from '../../common/test/utility';
|
||||
import { ADMIN_ROLES, USER_ROLES } from '../../constants';
|
||||
import {
|
||||
makeTestAccount,
|
||||
makeTestSimpleAccount,
|
||||
makeTestUser,
|
||||
} from '../../common/test/utility';
|
||||
import { ADMIN_ROLES, TASK_STATUS, USER_ROLES } from '../../constants';
|
||||
import { makeTestingModule } from '../../common/test/modules';
|
||||
import { createSortCriteria } from '../users/test/utility';
|
||||
|
||||
describe('TasksService', () => {
|
||||
it('タスク一覧を取得できる(admin)', async () => {
|
||||
@ -2460,3 +2466,480 @@ describe('cancel', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNextTask', () => {
|
||||
let source: DataSource | null = null;
|
||||
beforeEach(async () => {
|
||||
source = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: ':memory:',
|
||||
logging: false,
|
||||
entities: [__dirname + '/../../**/*.entity{.ts,.js}'],
|
||||
synchronize: true, // trueにすると自動的にmigrationが行われるため注意
|
||||
});
|
||||
return source.initialize();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (!source) return;
|
||||
await source.destroy();
|
||||
source = null;
|
||||
});
|
||||
|
||||
it('次タスクを取得できる(JobNumber順)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC');
|
||||
|
||||
const { taskId: taskId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000003',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId3, typistUserId);
|
||||
|
||||
const { taskId: taskId2, audioFileId: audioFileId2 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000002',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId2, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
const nextAudioFileId = await service.getNextTask(
|
||||
context,
|
||||
typistExternalId,
|
||||
audioFileId2,
|
||||
);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
expect(nextAudioFileId).toEqual(audioFileId3);
|
||||
}
|
||||
});
|
||||
|
||||
it('次タスクを取得できる(JobNumber順+優先度)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC');
|
||||
|
||||
const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'00',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'00',
|
||||
'00000003',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId3, typistUserId);
|
||||
|
||||
const { taskId: taskId2 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000002',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId2, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
const nextAudioFileId = await service.getNextTask(
|
||||
context,
|
||||
typistExternalId,
|
||||
audioFileId1,
|
||||
);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
expect(nextAudioFileId).toEqual(audioFileId3);
|
||||
}
|
||||
});
|
||||
|
||||
it('次タスクを取得できる(JobNumber順、先頭)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC');
|
||||
|
||||
const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000003',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId3, typistUserId);
|
||||
|
||||
const { taskId: taskId2 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'',
|
||||
'01',
|
||||
'00000002',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId2, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
const nextAudioFileId = await service.getNextTask(
|
||||
context,
|
||||
typistExternalId,
|
||||
audioFileId3,
|
||||
);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
expect(nextAudioFileId).toEqual(audioFileId1);
|
||||
}
|
||||
});
|
||||
|
||||
it('次タスクを取得できる(Worktype順)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'WORK_TYPE', 'ASC');
|
||||
|
||||
const { taskId: taskId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype1',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const { taskId: taskId3, audioFileId: audioFileId3 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype2',
|
||||
'01',
|
||||
'00000003',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId3, typistUserId);
|
||||
|
||||
const { taskId: taskId2, audioFileId: audioFileId2 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype3',
|
||||
'01',
|
||||
'00000002',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId2, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
const nextAudioFileId = await service.getNextTask(
|
||||
context,
|
||||
typistExternalId,
|
||||
audioFileId3,
|
||||
);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
expect(nextAudioFileId).toEqual(audioFileId2);
|
||||
}
|
||||
});
|
||||
|
||||
it('次タスクを取得できる(Status順)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'STATUS', 'ASC');
|
||||
|
||||
const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype1',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const { taskId: taskId3 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype2',
|
||||
'01',
|
||||
'00000003',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId3, typistUserId);
|
||||
|
||||
const { taskId: taskId2, audioFileId: audioFileId2 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype3',
|
||||
'01',
|
||||
'00000002',
|
||||
TASK_STATUS.PENDING,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId2, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
const nextAudioFileId = await service.getNextTask(
|
||||
context,
|
||||
typistExternalId,
|
||||
audioFileId2,
|
||||
);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
expect(nextAudioFileId).toEqual(audioFileId1);
|
||||
}
|
||||
});
|
||||
|
||||
it('次タスクが存在しない場合undefinedを返す(JobNumber順)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'JOB_NUMBER', 'ASC');
|
||||
|
||||
const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype1',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
const nextAudioFileId = await service.getNextTask(
|
||||
context,
|
||||
typistExternalId,
|
||||
audioFileId1,
|
||||
);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
{
|
||||
expect(nextAudioFileId).toEqual(undefined);
|
||||
}
|
||||
});
|
||||
it('指定タスクが存在しない場合エラーを返す(JobNumber順)', async () => {
|
||||
if (!source) fail();
|
||||
const module = await makeTestingModule(source);
|
||||
if (!module) fail();
|
||||
// 第五階層のアカウント作成
|
||||
const { account, admin } = await makeTestAccount(source, { tier: 5 });
|
||||
const { id: authorUserId } = await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
author_id: 'AUTHOR_ID',
|
||||
external_id: 'author-user-external-id',
|
||||
role: USER_ROLES.AUTHOR,
|
||||
});
|
||||
const { id: typistUserId, external_id: typistExternalId } =
|
||||
await makeTestUser(source, {
|
||||
account_id: account.id,
|
||||
external_id: 'typist-user-external-id',
|
||||
role: USER_ROLES.TYPIST,
|
||||
});
|
||||
|
||||
await createSortCriteria(source, typistUserId, 'WORK_TYPE', 'ASC');
|
||||
|
||||
const { taskId: taskId1, audioFileId: audioFileId1 } = await createTask(
|
||||
source,
|
||||
account.id,
|
||||
authorUserId,
|
||||
'MY_AUTHOR_ID',
|
||||
'worktype1',
|
||||
'01',
|
||||
'00000001',
|
||||
TASK_STATUS.UPLOADED,
|
||||
);
|
||||
await createCheckoutPermissions(source, taskId1, typistUserId);
|
||||
|
||||
const service = module.get<TasksService>(TasksService);
|
||||
const context = makeContext(admin.external_id);
|
||||
|
||||
// 実行結果が正しいか確認
|
||||
try {
|
||||
await service.getNextTask(context, typistExternalId, audioFileId1 + 1);
|
||||
fail();
|
||||
} catch (e) {
|
||||
if (e instanceof HttpException) {
|
||||
expect(e.getStatus()).toEqual(HttpStatus.BAD_REQUEST);
|
||||
expect(e.getResponse()).toEqual(makeErrorResponse('E010603'));
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -157,6 +157,83 @@ export class TasksService {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.getTasks.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完了したタスクの次のタスクを取得します
|
||||
* @param context
|
||||
* @param externalId
|
||||
* @param fileId
|
||||
* @returns next task
|
||||
*/
|
||||
async getNextTask(
|
||||
context: Context,
|
||||
externalId: string,
|
||||
fileId: number,
|
||||
): Promise<number | undefined> {
|
||||
this.logger.log(
|
||||
`[IN] [${context.trackingId}] ${this.getNextTask.name} | params: { externalId: ${externalId}, fileId: ${fileId} };`,
|
||||
);
|
||||
|
||||
try {
|
||||
const { account_id: accountId, id } =
|
||||
await this.usersRepository.findUserByExternalId(externalId);
|
||||
|
||||
// タスク一覧を取得する
|
||||
const tasks = await this.taskRepository.getSortedTasks(
|
||||
accountId,
|
||||
id,
|
||||
fileId,
|
||||
);
|
||||
|
||||
// 指定タスクのインデックスを取得する
|
||||
const targetTaskIndex = tasks.findIndex(
|
||||
(x) => x.audio_file_id === fileId,
|
||||
);
|
||||
|
||||
// 指定したタスクが見つからない場合はエラーとする(リポジトリからは必ず取得できる想定)
|
||||
if (targetTaskIndex === -1) {
|
||||
throw new TasksNotFoundError(`task not found: ${fileId}`);
|
||||
}
|
||||
|
||||
// ソート順に並んだタスクについて、指定した完了済みタスクの次のタスクを取得する
|
||||
let nextTaskIndex = targetTaskIndex + 1;
|
||||
|
||||
// 次のタスクがない場合は先頭のタスクを返す
|
||||
if (tasks.length - 1 < nextTaskIndex) {
|
||||
nextTaskIndex = 0;
|
||||
}
|
||||
|
||||
const nextTask = tasks[nextTaskIndex];
|
||||
|
||||
// 先頭のタスクが指定した完了済みタスクの場合は次のタスクがないためundefinedを返す
|
||||
return nextTask.audio_file_id === fileId
|
||||
? undefined
|
||||
: nextTask.audio_file_id;
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
if (e instanceof Error) {
|
||||
switch (e.constructor) {
|
||||
case TasksNotFoundError:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E010603'),
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
default:
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
}
|
||||
throw new HttpException(
|
||||
makeErrorResponse('E009999'),
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
} finally {
|
||||
this.logger.log(`[OUT] [${context.trackingId}] ${this.getNextTask.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定した音声ファイルに紐づくタスクをcheckoutする
|
||||
* @param audioFileId
|
||||
|
||||
@ -110,7 +110,7 @@ export const createTask = async (
|
||||
jobNumber: string,
|
||||
status: string,
|
||||
typist_user_id?: number | undefined,
|
||||
): Promise<{ taskId: number }> => {
|
||||
): Promise<{ taskId: number; audioFileId: number }> => {
|
||||
const { identifiers: audioFileIdentifiers } = await datasource
|
||||
.getRepository(AudioFile)
|
||||
.insert({
|
||||
@ -144,7 +144,7 @@ export const createTask = async (
|
||||
created_at: new Date(),
|
||||
});
|
||||
const task = taskIdentifiers.pop() as Task;
|
||||
return { taskId: task.id };
|
||||
return { taskId: task.id, audioFileId: audioFile.id };
|
||||
};
|
||||
/**
|
||||
*
|
||||
@ -162,8 +162,8 @@ export const createCheckoutPermissions = async (
|
||||
): Promise<void> => {
|
||||
await datasource.getRepository(CheckoutPermission).insert({
|
||||
task_id: task_id,
|
||||
user_id: user_id,
|
||||
user_group_id: user_group_id,
|
||||
user_id: user_id ?? null,
|
||||
user_group_id: user_group_id ?? null,
|
||||
});
|
||||
};
|
||||
/**
|
||||
|
||||
@ -190,6 +190,8 @@ export class TasksResponse {
|
||||
}
|
||||
export class AudioNextRequest {
|
||||
@ApiProperty({ description: '文字起こし完了したタスクの音声ファイルID' })
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
endedFileId: number;
|
||||
}
|
||||
|
||||
|
||||
@ -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<Promise<Aadb2cUser | undefined>, []>()
|
||||
.fn<Promise<AdB2cUser | undefined>, []>()
|
||||
.mockRejectedValue(getUser)
|
||||
: jest
|
||||
.fn<Promise<Aadb2cUser | undefined>, []>()
|
||||
.fn<Promise<AdB2cUser | undefined>, []>()
|
||||
.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,
|
||||
};
|
||||
|
||||
@ -35,6 +35,7 @@ import { License } from '../../../repositories/licenses/entity/license.entity';
|
||||
import { AdB2cMockValue, makeAdB2cServiceMock } from './users.service.mock';
|
||||
import { AdB2cService } from '../../../gateways/adb2c/adb2c.service';
|
||||
import { LICENSE_ALLOCATED_STATUS, LICENSE_TYPE } from '../../../constants';
|
||||
import { SortCriteria } from '../../../repositories/sort_criteria/entity/sort_criteria.entity';
|
||||
|
||||
export const getLicenses = async (
|
||||
datasource: DataSource,
|
||||
@ -164,3 +165,16 @@ export const makeTestingModuleWithAdb2c = async (
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const createSortCriteria = async (
|
||||
datasource: DataSource,
|
||||
userId: number,
|
||||
parameter: string,
|
||||
direction: string,
|
||||
): Promise<void> => {
|
||||
await datasource.getRepository(SortCriteria).insert({
|
||||
user_id: userId,
|
||||
parameter: parameter,
|
||||
direction: direction,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -1,13 +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 } from '../../constants';
|
||||
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';
|
||||
@ -34,9 +37,14 @@ export class AdB2cService {
|
||||
this.configService.getOrThrow<string>('TENANT_NAME');
|
||||
private readonly flowName =
|
||||
this.configService.getOrThrow<string>('SIGNIN_FLOW_NAME');
|
||||
private readonly ttl =
|
||||
this.configService.getOrThrow<number>('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<string>('ADB2C_TENANT_ID'),
|
||||
@ -175,11 +183,25 @@ export class AdB2cService {
|
||||
* @param externalId 外部ユーザーID
|
||||
* @returns ユーザ情報
|
||||
*/
|
||||
async getUser(externalId: string): Promise<Aadb2cUser> {
|
||||
async getUser(externalId: string): Promise<AdB2cUser> {
|
||||
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<AdB2cUser>(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;
|
||||
@ -202,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<AdB2cUser>(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[] = [];
|
||||
@ -220,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;
|
||||
@ -248,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;
|
||||
@ -267,12 +322,42 @@ export class AdB2cService {
|
||||
);
|
||||
|
||||
try {
|
||||
// 複数ユーザーを一括削除する方法が不明なため、rate limitの懸念があるのを承知のうえ単一削除の繰り返しで実装
|
||||
// TODO 一括削除する方法が判明したら修正する
|
||||
// https://learn.microsoft.com/en-us/graph/api/user-delete?view=graph-rest-1.0&tabs=javascript#example
|
||||
externalIds.map(
|
||||
async (x) => await this.graphClient.api(`users/${x}`).delete(),
|
||||
// 複数ユーザーを一括削除する方法がないため、1人ずつで削除を行う(rate limitに大きな影響がないこと確認済)
|
||||
const results = await Promise.allSettled(
|
||||
externalIds.map(async (externalId) => {
|
||||
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}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 失敗したプロミスのエラーをログに記録
|
||||
results.forEach((result, index) => {
|
||||
// statusがrejectedでない場合は、エラーが発生していないためログに記録しない
|
||||
if (result.status !== 'rejected') {
|
||||
return;
|
||||
}
|
||||
|
||||
const failedId = externalIds[index];
|
||||
if (isPromiseRejectedResult(result)) {
|
||||
const error = result.reason.toString();
|
||||
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED}[${context.trackingId}] Failed to delete user ${failedId}: ${error}`,
|
||||
);
|
||||
} else {
|
||||
this.logger.error(
|
||||
`${MANUAL_RECOVERY_REQUIRED}[${context.trackingId}] Failed to delete user ${failedId}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error(`error=${e}`);
|
||||
throw e;
|
||||
@ -282,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の制限数
|
||||
|
||||
10
dictation_server/src/gateways/adb2c/utils/utils.ts
Normal file
10
dictation_server/src/gateways/adb2c/utils/utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export const isPromiseRejectedResult = (
|
||||
data: unknown,
|
||||
): data is PromiseRejectedResult => {
|
||||
return (
|
||||
data !== null &&
|
||||
typeof data === 'object' &&
|
||||
'status' in data &&
|
||||
'reason' in data
|
||||
);
|
||||
};
|
||||
8
dictation_server/src/gateways/redis/redis.module.ts
Normal file
8
dictation_server/src/gateways/redis/redis.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { CacheModule, Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
||||
110
dictation_server/src/gateways/redis/redis.service.ts
Normal file
110
dictation_server/src/gateways/redis/redis.service.ts
Normal file
@ -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<void> {
|
||||
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<T>(
|
||||
records: { key: string; value: T }[],
|
||||
ttl?: number | undefined,
|
||||
): Promise<void> {
|
||||
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<T>(key: string): Promise<T | undefined> {
|
||||
try {
|
||||
const value = await this.cacheManager.get<T>(key);
|
||||
return value;
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 複数のキーに対して、対応する値を取得する。
|
||||
* キーに対応する値がなかった場合、valueにはnullがセットされる。
|
||||
* @param keys キーの配列
|
||||
* @returns キーとそのキーに対応する値のペアの配列
|
||||
*/
|
||||
async mget<T>(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<void> {
|
||||
try {
|
||||
await this.cacheManager.del(key);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@ import {
|
||||
Column,
|
||||
PrimaryGeneratedColumn,
|
||||
JoinColumn,
|
||||
OneToOne,
|
||||
ManyToOne,
|
||||
} from 'typeorm';
|
||||
|
||||
@ -25,11 +24,11 @@ export class CheckoutPermission {
|
||||
@Column({ nullable: true, type: 'bigint', transformer: bigintTransformer })
|
||||
user_group_id: number | null;
|
||||
|
||||
@OneToOne(() => User, (user) => user.id)
|
||||
@ManyToOne(() => User, (user) => user.id)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User | null;
|
||||
|
||||
@OneToOne(() => UserGroup, (group) => group.id)
|
||||
@ManyToOne(() => UserGroup, (group) => group.id)
|
||||
@JoinColumn({ name: 'user_group_id' })
|
||||
user_group: UserGroup | null;
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@ import { CheckoutPermission } from '../checkout_permissions/entity/checkout_perm
|
||||
import {
|
||||
SortDirection,
|
||||
TaskListSortableAttribute,
|
||||
isSortDirection,
|
||||
isTaskListSortableAttribute,
|
||||
} from '../../common/types/sort';
|
||||
import { UserGroupMember } from '../user_groups/entity/user_group_member.entity';
|
||||
import { Assignee } from '../../features/tasks/types/types';
|
||||
@ -32,6 +34,7 @@ import {
|
||||
} from './errors/types';
|
||||
import { Roles } from '../../common/types/role';
|
||||
import { TaskStatus, isTaskStatus } from '../../common/types/taskStatus';
|
||||
import { SortCriteria } from '../sort_criteria/entity/sort_criteria.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TasksRepositoryService {
|
||||
@ -853,6 +856,102 @@ export class TasksRepositoryService {
|
||||
return await checkoutPermissionRepo.save(checkoutPermissions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 対象ユーザーのソート順でソートしたタスク一覧を取得します(指定タスクとユーザが着手可能なタスクの一覧を取得します)
|
||||
* @param accountId
|
||||
* @param userId
|
||||
* @param audioFileId
|
||||
* @returns sorted tasks
|
||||
*/
|
||||
async getSortedTasks(
|
||||
accountId: number,
|
||||
userId: number,
|
||||
audioFileId: number,
|
||||
): Promise<Task[]> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const taskRepo = entityManager.getRepository(Task);
|
||||
const sortRepo = entityManager.getRepository(SortCriteria);
|
||||
|
||||
const sort = await sortRepo.findOne({ where: { user_id: userId } });
|
||||
|
||||
// 運用上はあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!sort) {
|
||||
throw new Error(`sort criteria not found. userId: ${userId}`);
|
||||
}
|
||||
|
||||
const { direction, parameter } = sort;
|
||||
//型チェック
|
||||
if (
|
||||
!isTaskListSortableAttribute(parameter) ||
|
||||
!isSortDirection(direction)
|
||||
) {
|
||||
throw new Error(
|
||||
`The value stored in the DB is invalid. parameter: ${parameter}, direction: ${direction}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 指定した音声ファイルIDのタスクを取得
|
||||
const targetTask = await taskRepo.findOne({
|
||||
where: {
|
||||
account_id: accountId,
|
||||
audio_file_id: audioFileId,
|
||||
status: In([
|
||||
TASK_STATUS.PENDING,
|
||||
TASK_STATUS.FINISHED,
|
||||
TASK_STATUS.UPLOADED,
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
if (!targetTask) {
|
||||
throw new TasksNotFoundError(
|
||||
`target task not found. audioFileId: ${audioFileId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const groupMemberRepo = entityManager.getRepository(UserGroupMember);
|
||||
// ユーザーの所属するすべてのグループを列挙
|
||||
const groups = await groupMemberRepo.find({ where: { user_id: userId } });
|
||||
// ユーザーの所属するすべてのグループIDを列挙
|
||||
const groupIds = groups.map((member) => member.user_group_id);
|
||||
|
||||
const checkoutRepo = entityManager.getRepository(CheckoutPermission);
|
||||
// ユーザーに対するチェックアウト権限、またはユーザーの所属するユーザーグループのチェックアウト権限を取得
|
||||
const related = await checkoutRepo.find({
|
||||
where: [
|
||||
// ユーザーがチェックアウト可能である
|
||||
{ user_id: userId },
|
||||
// ユーザーの所属するユーザーグループがチェックアウト可能である
|
||||
{ user_group_id: In(groupIds) },
|
||||
],
|
||||
});
|
||||
|
||||
// ユーザー本人、またはユーザーが所属するユーザーグループがチェックアウト可能なタスクIDの一覧を作成
|
||||
const relatedTaskIds = related.map((permission) => permission.task_id);
|
||||
|
||||
const order = makeOrder(parameter, direction);
|
||||
|
||||
// 引数の音声ファイルIDで指定したタスクとユーザが着手可能なタスクの一覧を取得
|
||||
const tasks = await taskRepo.find({
|
||||
where: [
|
||||
{
|
||||
account_id: accountId,
|
||||
id: targetTask.id,
|
||||
},
|
||||
{
|
||||
account_id: accountId,
|
||||
status: In([TASK_STATUS.UPLOADED, TASK_STATUS.PENDING]),
|
||||
// TypistまたはTypistが所属するユーザーグループが割り当て可能になっているTaskを取得
|
||||
id: In(relatedTaskIds),
|
||||
},
|
||||
],
|
||||
order: order,
|
||||
});
|
||||
|
||||
return tasks;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ソート用オブジェクトを生成する
|
||||
|
||||
@ -12,3 +12,5 @@ export class EncryptionPasswordNeedError extends Error {}
|
||||
export class TermInfoNotFoundError extends Error {}
|
||||
// 利用規約バージョンパラメータ不在エラー
|
||||
export class UpdateTermsVersionNotSetError extends Error {}
|
||||
// 代行操作不許可エラー
|
||||
export class DelegationNotAllowedError extends Error {}
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
EncryptionPasswordNeedError,
|
||||
TermInfoNotFoundError,
|
||||
UpdateTermsVersionNotSetError,
|
||||
DelegationNotAllowedError,
|
||||
} from './errors/types';
|
||||
import {
|
||||
LICENSE_ALLOCATED_STATUS,
|
||||
@ -27,7 +28,11 @@ import { License } from '../licenses/entity/license.entity';
|
||||
import { NewTrialLicenseExpirationDate } from '../../features/licenses/types/types';
|
||||
import { Term } from '../terms/entity/term.entity';
|
||||
import { TermsCheckInfo } from '../../features/auth/types/types';
|
||||
import { AccountNotFoundError } from '../accounts/errors/types';
|
||||
import {
|
||||
AccountNotFoundError,
|
||||
AdminUserNotFoundError,
|
||||
} from '../accounts/errors/types';
|
||||
import { Account } from '../accounts/entity/account.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersRepositoryService {
|
||||
@ -344,7 +349,7 @@ export class UsersRepositoryService {
|
||||
throw new AccountNotFoundError('Account is Not Found.');
|
||||
}
|
||||
|
||||
const dbUsers = await this.dataSource.getRepository(User).find({
|
||||
const dbUsers = await repo.find({
|
||||
relations: {
|
||||
userGroupMembers: {
|
||||
userGroup: true,
|
||||
@ -533,4 +538,110 @@ export class UsersRepositoryService {
|
||||
await userRepo.update({ id: user.id }, user);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作対象のユーザー情報を取得する
|
||||
* @param delegateAccountId 代行操作者のアカウントID
|
||||
* @param originAccountId 代行操作対象のアカウントID
|
||||
* @returns delegate accounts
|
||||
*/
|
||||
async findDelegateUser(
|
||||
delegateAccountId: number,
|
||||
originAccountId: number,
|
||||
): Promise<User> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const accountRepo = entityManager.getRepository(Account);
|
||||
|
||||
// 代行操作対象のアカウントを取得 ※親アカウントが代行操作者のアカウントIDと一致すること
|
||||
const account = await accountRepo.findOne({
|
||||
where: {
|
||||
id: originAccountId,
|
||||
parent_account_id: delegateAccountId,
|
||||
tier: TIERS.TIER5,
|
||||
},
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
throw new AccountNotFoundError(
|
||||
`Account is not found. originAccountId: ${originAccountId}, delegateAccountId: ${delegateAccountId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 代行操作が許可されていない場合はエラー
|
||||
if (!account.delegation_permission) {
|
||||
throw new DelegationNotAllowedError(
|
||||
`Delegation is not allowed. id: ${originAccountId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const adminUserId = account.primary_admin_user_id;
|
||||
|
||||
// 運用上、代行操作対象アカウントの管理者ユーザーがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!adminUserId) {
|
||||
throw new Error(`Admin user is not found. id: ${originAccountId}`);
|
||||
}
|
||||
|
||||
// 代行操作対象のアカウントの管理者ユーザーを取得
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const primaryUser = await userRepo.findOne({
|
||||
where: {
|
||||
account_id: originAccountId,
|
||||
id: adminUserId,
|
||||
},
|
||||
relations: {
|
||||
account: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 運用上、代行操作対象アカウントの管理者ユーザーがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!primaryUser) {
|
||||
throw new Error(`Admin user is not found. id: ${originAccountId}`);
|
||||
}
|
||||
|
||||
return primaryUser;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 代行操作対象のユーザーの所属するアカウントの代行操作が許可されているか
|
||||
* @param delegateAccountId 代行操作者のアカウントID
|
||||
* @param originAccountId 代行操作対象のアカウントID
|
||||
* @returns delegate accounts
|
||||
*/
|
||||
async isAllowDelegationPermission(
|
||||
delegateAccountId: number,
|
||||
originUserExternalId: string,
|
||||
): Promise<boolean> {
|
||||
return await this.dataSource.transaction(async (entityManager) => {
|
||||
const userRepo = entityManager.getRepository(User);
|
||||
const primaryUser = await userRepo.findOne({
|
||||
where: {
|
||||
external_id: originUserExternalId,
|
||||
account: {
|
||||
parent_account_id: delegateAccountId,
|
||||
tier: TIERS.TIER5,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
account: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!primaryUser) {
|
||||
throw new AdminUserNotFoundError(
|
||||
`Admin user is not found. externalId: ${originUserExternalId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const originAccount = primaryUser.account;
|
||||
|
||||
// 運用上、アカウントがいないことはあり得ないが、プログラム上発生しうるのでエラーとして処理
|
||||
if (!originAccount) {
|
||||
throw new Error(`Account is Not Found. id: ${primaryUser.account_id}`);
|
||||
}
|
||||
|
||||
// 代行操作の許可の有無を返却
|
||||
return originAccount.delegation_permission;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user