Merge branch 'release-ccb' into hotfix-task4228
This commit is contained in:
commit
b997b928b8
312
azure-pipelines-staging-ccb.yml
Normal file
312
azure-pipelines-staging-ccb.yml
Normal file
@ -0,0 +1,312 @@
|
||||
# Pipeline側でKeyVaultやDocker、AppService等に対する操作権限を持ったServiceConenctionを作成しておくこと
|
||||
# また、環境変数 STATIC_DICTATION_DEPLOYMENT_TOKEN の値として静的WebAppsのデプロイトークンを設定しておくこと
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- release-ccb
|
||||
tags:
|
||||
include:
|
||||
- stage-*
|
||||
|
||||
jobs:
|
||||
- job: initialize
|
||||
displayName: Initialize
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
persistCredentials: true
|
||||
- script: |
|
||||
git fetch origin release-ccb:release-ccb
|
||||
if git merge-base --is-ancestor $(Build.SourceVersion) release-ccb; then
|
||||
echo "This commit is in the release-ccb branch."
|
||||
else
|
||||
echo "This commit is not in the release-ccb branch."
|
||||
exit 1
|
||||
fi
|
||||
displayName: 'タグが付けられたCommitがrelease-ccbブランチに存在するか確認'
|
||||
- job: backend_test
|
||||
dependsOn: initialize
|
||||
condition: succeeded('initialize')
|
||||
displayName: UnitTest
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: Bash@3
|
||||
displayName: Bash Script (Test)
|
||||
inputs:
|
||||
targetType: inline
|
||||
workingDirectory: dictation_server/.devcontainer
|
||||
script: |
|
||||
docker-compose -f pipeline-docker-compose.yml build
|
||||
docker-compose -f pipeline-docker-compose.yml up -d
|
||||
docker-compose exec -T dictation_server sudo npm ci
|
||||
docker-compose exec -T dictation_server sudo npm run migrate:up:test
|
||||
docker-compose exec -T dictation_server sudo npm run test
|
||||
- job: backend_build
|
||||
dependsOn: backend_test
|
||||
condition: succeeded('backend_test')
|
||||
displayName: Build And Push Backend Image
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: Npm@1
|
||||
displayName: npm ci
|
||||
inputs:
|
||||
command: ci
|
||||
workingDir: dictation_server
|
||||
verbose: false
|
||||
- task: Docker@0
|
||||
displayName: build
|
||||
inputs:
|
||||
azureSubscriptionEndpoint: 'omds-service-connection-stg'
|
||||
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
|
||||
dockerFile: DockerfileServerDictation.dockerfile
|
||||
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
|
||||
buildArguments: |
|
||||
BUILD_VERSION=$(Build.SourceVersion)
|
||||
- task: Docker@0
|
||||
displayName: push
|
||||
inputs:
|
||||
azureSubscriptionEndpoint: 'omds-service-connection-stg'
|
||||
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
|
||||
action: Push an image
|
||||
imageName: odmscloud/staging/dictation:$(Build.SourceVersion)
|
||||
- job: frontend_build_staging
|
||||
dependsOn: backend_build
|
||||
condition: succeeded('backend_build')
|
||||
displayName: Build Frontend Files(staging)
|
||||
variables:
|
||||
storageAccountName: saomdspipeline
|
||||
environment: staging
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: Npm@1
|
||||
displayName: npm ci
|
||||
inputs:
|
||||
command: ci
|
||||
workingDir: dictation_client
|
||||
verbose: false
|
||||
- task: Bash@3
|
||||
displayName: Bash Script
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: cd dictation_client && npm run build:stg
|
||||
- task: ArchiveFiles@2
|
||||
inputs:
|
||||
rootFolderOrFile: dictation_client/build
|
||||
includeRootFolder: false
|
||||
archiveType: 'zip'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip'
|
||||
replaceExistingArchive: true
|
||||
- task: AzureCLI@2
|
||||
inputs:
|
||||
azureSubscription: 'omds-service-connection-stg'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
az storage blob upload \
|
||||
--auth-mode login \
|
||||
--account-name $(storageAccountName) \
|
||||
--container-name $(environment) \
|
||||
--name $(Build.SourceVersion).zip \
|
||||
--type block \
|
||||
--overwrite \
|
||||
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
|
||||
- job: function_test
|
||||
dependsOn: frontend_build_staging
|
||||
condition: succeeded('frontend_build_staging')
|
||||
displayName: UnitTest
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: Bash@3
|
||||
displayName: Bash Script (Test)
|
||||
inputs:
|
||||
targetType: inline
|
||||
workingDirectory: dictation_function/.devcontainer
|
||||
script: |
|
||||
docker-compose -f pipeline-docker-compose.yml build
|
||||
docker-compose -f pipeline-docker-compose.yml up -d
|
||||
docker-compose exec -T dictation_function sudo npm ci
|
||||
docker-compose exec -T dictation_function sudo npm run test
|
||||
- job: function_build
|
||||
dependsOn: function_test
|
||||
condition: succeeded('function_test')
|
||||
displayName: Build And Push Function Image
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: Npm@1
|
||||
displayName: npm ci
|
||||
inputs:
|
||||
command: ci
|
||||
workingDir: dictation_function
|
||||
verbose: false
|
||||
- task: Docker@0
|
||||
displayName: build
|
||||
inputs:
|
||||
azureSubscriptionEndpoint: 'omds-service-connection-stg'
|
||||
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
|
||||
dockerFile: DockerfileFunctionDictation.dockerfile
|
||||
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
|
||||
buildArguments: |
|
||||
BUILD_VERSION=$(Build.SourceVersion)
|
||||
- task: Docker@0
|
||||
displayName: push
|
||||
inputs:
|
||||
azureSubscriptionEndpoint: 'omds-service-connection-stg'
|
||||
azureContainerRegistry: '{"loginServer":"crodmsregistrymaintenance.azurecr.io", "id" : "/subscriptions/108fb131-cdca-4729-a2be-e5bd8c0b3ba7/resourceGroups/maintenance-rg/providers/Microsoft.ContainerRegistry/registries/crOdmsRegistryMaintenance"}'
|
||||
action: Push an image
|
||||
imageName: odmscloud/staging/dictation_function:$(Build.SourceVersion)
|
||||
- job: backend_deploy
|
||||
dependsOn: function_build
|
||||
condition: succeeded('function_build')
|
||||
displayName: Backend Deploy
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: AzureWebAppContainer@1
|
||||
inputs:
|
||||
azureSubscription: 'omds-service-connection-stg'
|
||||
appName: 'app-odms-dictation-stg'
|
||||
deployToSlotOrASE: true
|
||||
resourceGroupName: 'stg-application-rg'
|
||||
slotName: 'staging'
|
||||
containers: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation:$(Build.SourceVersion)'
|
||||
- job: frontend_deploy
|
||||
dependsOn: backend_deploy
|
||||
condition: succeeded('backend_deploy')
|
||||
displayName: Deploy Frontend Files
|
||||
variables:
|
||||
storageAccountName: saomdspipeline
|
||||
environment: staging
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: AzureCLI@2
|
||||
inputs:
|
||||
azureSubscription: 'omds-service-connection-stg'
|
||||
scriptType: 'bash'
|
||||
scriptLocation: 'inlineScript'
|
||||
inlineScript: |
|
||||
az storage blob download \
|
||||
--auth-mode login \
|
||||
--account-name $(storageAccountName) \
|
||||
--container-name $(environment) \
|
||||
--name $(Build.SourceVersion).zip \
|
||||
--file $(Build.SourcesDirectory)/$(Build.SourceVersion).zip
|
||||
- task: Bash@3
|
||||
displayName: Bash Script
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: unzip $(Build.SourcesDirectory)/$(Build.SourceVersion).zip -d $(Build.SourcesDirectory)/$(Build.SourceVersion)
|
||||
- task: AzureStaticWebApp@0
|
||||
displayName: 'Static Web App: '
|
||||
inputs:
|
||||
workingDirectory: '$(Build.SourcesDirectory)'
|
||||
app_location: '/$(Build.SourceVersion)'
|
||||
config_file_location: /dictation_client
|
||||
skip_app_build: true
|
||||
skip_api_build: true
|
||||
is_static_export: false
|
||||
verbose: false
|
||||
azure_static_web_apps_api_token: $(STATIC_DICTATION_DEPLOYMENT_TOKEN)
|
||||
- job: function_deploy
|
||||
dependsOn: frontend_deploy
|
||||
condition: succeeded('frontend_deploy')
|
||||
displayName: Function Deploy
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: AzureFunctionAppContainer@1
|
||||
inputs:
|
||||
azureSubscription: 'omds-service-connection-stg'
|
||||
appName: 'func-odms-dictation-stg'
|
||||
imageName: 'crodmsregistrymaintenance.azurecr.io/odmscloud/staging/dictation_function:$(Build.SourceVersion)'
|
||||
- job: smoke_test
|
||||
dependsOn: function_deploy
|
||||
condition: succeeded('function_deploy')
|
||||
displayName: 'smoke test'
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
# スモークテスト用にjobを確保
|
||||
- job: swap_slot
|
||||
dependsOn: smoke_test
|
||||
condition: succeeded('smoke_test')
|
||||
displayName: 'Swap Staging and Production'
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: AzureAppServiceManage@0
|
||||
displayName: 'Azure App Service Manage: app-odms-dictation-stg'
|
||||
inputs:
|
||||
azureSubscription: 'omds-service-connection-stg'
|
||||
action: 'Swap Slots'
|
||||
WebAppName: 'app-odms-dictation-stg'
|
||||
ResourceGroupName: 'stg-application-rg'
|
||||
SourceSlot: 'staging'
|
||||
SwapWithProduction: true
|
||||
- job: migration
|
||||
dependsOn: swap_slot
|
||||
condition: succeeded('swap_slot')
|
||||
displayName: DB migration
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: AzureKeyVault@2
|
||||
displayName: 'Azure Key Vault: kv-odms-secret-stg'
|
||||
inputs:
|
||||
ConnectedServiceName: 'omds-service-connection-stg'
|
||||
KeyVaultName: kv-odms-secret-stg
|
||||
- task: CmdLine@2
|
||||
displayName: migration
|
||||
inputs:
|
||||
script: >2
|
||||
# DB接続情報書き換え
|
||||
sed -i -e "s/DB_NAME_CCB/$(db-name-ccb)/g" ./dictation_server/db/dbconfig.yml
|
||||
sed -i -e "s/DB_PASS/$(admin-db-pass)/g" ./dictation_server/db/dbconfig.yml
|
||||
sed -i -e "s/DB_USERNAME/$(admin-db-user)/g" ./dictation_server/db/dbconfig.yml
|
||||
sed -i -e "s/DB_PORT/$(db-port)/g" ./dictation_server/db/dbconfig.yml
|
||||
sed -i -e "s/DB_HOST/$(db-host)/g" ./dictation_server/db/dbconfig.yml
|
||||
sql-migrate --version
|
||||
cat ./dictation_server/db/dbconfig.yml
|
||||
# migration実行
|
||||
sql-migrate up -config=./dictation_server/db/dbconfig.yml -env=ci_ccb
|
||||
@ -170,9 +170,29 @@ jobs:
|
||||
--type block \
|
||||
--overwrite \
|
||||
--file $(Build.ArtifactStagingDirectory)/$(Build.SourceVersion).zip
|
||||
- job: function_build
|
||||
- job: function_test
|
||||
dependsOn: frontend_build_production
|
||||
condition: succeeded('frontend_build_production')
|
||||
displayName: UnitTest
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- checkout: self
|
||||
clean: true
|
||||
fetchDepth: 1
|
||||
- task: Bash@3
|
||||
displayName: Bash Script (Test)
|
||||
inputs:
|
||||
targetType: inline
|
||||
workingDirectory: dictation_function/.devcontainer
|
||||
script: |
|
||||
docker-compose -f pipeline-docker-compose.yml build
|
||||
docker-compose -f pipeline-docker-compose.yml up -d
|
||||
docker-compose exec -T dictation_function sudo npm ci
|
||||
docker-compose exec -T dictation_function sudo npm run test
|
||||
- job: function_build
|
||||
dependsOn: function_test
|
||||
condition: succeeded('function_test')
|
||||
displayName: Build And Push Function Image
|
||||
pool:
|
||||
name: odms-deploy-pipeline
|
||||
@ -186,32 +206,6 @@ jobs:
|
||||
command: ci
|
||||
workingDir: dictation_function
|
||||
verbose: false
|
||||
- task: AzureKeyVault@2
|
||||
displayName: 'Azure Key Vault: kv-odms-secret-stg'
|
||||
inputs:
|
||||
ConnectedServiceName: 'omds-service-connection-stg'
|
||||
KeyVaultName: kv-odms-secret-stg
|
||||
SecretsFilter: '*'
|
||||
- task: Bash@3
|
||||
displayName: Bash Script (Test)
|
||||
inputs:
|
||||
targetType: inline
|
||||
script: |
|
||||
cd dictation_function
|
||||
npm run test
|
||||
env:
|
||||
TENANT_NAME: xxxxxxxxxxxx
|
||||
SIGNIN_FLOW_NAME: xxxxxxxxxxxx
|
||||
ADB2C_TENANT_ID: $(adb2c-tenant-id)
|
||||
ADB2C_CLIENT_ID: $(adb2c-client-id)
|
||||
ADB2C_CLIENT_SECRET: $(adb2c-client-secret)
|
||||
ADB2C_ORIGIN: xxxxxx
|
||||
SENDGRID_API_KEY: $(sendgrid-api-key)
|
||||
MAIL_FROM: xxxxxx
|
||||
APP_DOMAIN: http://localhost:8081/
|
||||
REDIS_HOST: xxxxxxxxxxxx
|
||||
REDIS_PORT: 0
|
||||
REDIS_PASSWORD: xxxxxxxxxxxx
|
||||
- task: Docker@0
|
||||
displayName: build
|
||||
inputs:
|
||||
|
||||
5
dictation_client/jest.config.js
Normal file
5
dictation_client/jest.config.js
Normal file
@ -0,0 +1,5 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
};
|
||||
6036
dictation_client/package-lock.json
generated
6036
dictation_client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"codegen": "sh codegen.sh",
|
||||
"lint": "eslint --cache . --ext .js,.ts,.tsx",
|
||||
"lint:fix": "npm run lint -- --fix"
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^2.33.0",
|
||||
@ -25,7 +26,6 @@
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^14.2.1",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^17.0.45",
|
||||
"@types/react": "^18.0.14",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
@ -38,6 +38,7 @@
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"luxon": "^3.3.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-google-recaptcha-v3": "^1.10.0",
|
||||
@ -56,8 +57,10 @@
|
||||
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
|
||||
"@mdx-js/react": "^2.1.2",
|
||||
"@openapitools/openapi-generator-cli": "^2.5.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/redux-mock-store": "^1.0.3",
|
||||
@ -67,16 +70,18 @@
|
||||
"babel-loader": "^8.2.5",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-prettier": "^8.10.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"license-checker": "^25.0.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.8",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"sass": "^1.58.3",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.1.4",
|
||||
"vite-plugin-env-compatible": "^1.1.1",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
18
dictation_client/src/assets/images/change_circle.svg
Normal file
18
dictation_client/src/assets/images/change_circle.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 28.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="レイヤー_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||
y="0px" viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#282828;}
|
||||
</style>
|
||||
<path class="st0" d="M24.1,38l5.7-5.7l-5.7-5.6L22,28.8l2.1,2.1c-0.9,0-1.8-0.1-2.7-0.4c-0.9-0.3-1.7-0.9-2.4-1.6
|
||||
c-0.7-0.7-1.2-1.4-1.5-2.3C17.2,25.8,17,24.9,17,24c0-0.6,0.1-1.1,0.2-1.7s0.4-1.1,0.6-1.7l-2.2-2.2c-0.6,0.8-1,1.7-1.2,2.6
|
||||
C14.1,22.1,14,23,14,24c0,1.3,0.2,2.5,0.8,3.8C15.2,29,16,30.1,17,31s2,1.7,3.2,2.2c1.2,0.5,2.4,0.7,3.7,0.8L22,35.9L24.1,38z
|
||||
M32.4,29.5c0.6-0.8,1-1.7,1.2-2.6C33.9,25.9,34,25,34,24c0-1.3-0.2-2.5-0.7-3.8s-1.2-2.4-2.2-3.3s-2.1-1.7-3.3-2.2
|
||||
c-1.2-0.5-2.5-0.7-3.7-0.7l1.9-1.9L23.9,10l-5.7,5.7l5.7,5.6l2.1-2.1L23.8,17c0.9,0,1.8,0.2,2.8,0.5s1.7,0.9,2.4,1.5
|
||||
s1.2,1.4,1.5,2.3c0.4,0.9,0.5,1.7,0.5,2.6c0,0.6-0.1,1.1-0.2,1.7c-0.1,0.6-0.4,1.1-0.6,1.6L32.4,29.5z M24,44
|
||||
c-2.7,0-5.3-0.5-7.8-1.6s-4.6-2.5-6.4-4.3s-3.2-3.9-4.3-6.4S4,26.7,4,24c0-2.8,0.5-5.4,1.6-7.8s2.5-4.5,4.3-6.3s3.9-3.2,6.4-4.3
|
||||
S21.3,4,24,4c2.8,0,5.4,0.5,7.8,1.6s4.6,2.5,6.4,4.3s3.2,3.9,4.3,6.3c1.1,2.4,1.6,5,1.6,7.8c0,2.7-0.5,5.3-1.6,7.8
|
||||
c-1,2.4-2.5,4.6-4.3,6.4s-3.9,3.2-6.4,4.3S26.8,44,24,44z M24,41c4.7,0,8.8-1.7,12-5c3.3-3.3,5-7.3,5-12c0-4.7-1.6-8.8-5-12.1
|
||||
c-3.3-3.3-7.3-5-12-5c-4.7,0-8.7,1.7-12,5S7,19.3,7,24c0,4.7,1.7,8.7,5,12C15.3,39.3,19.3,41,24,41z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
dictation_client/src/assets/images/shuffle.svg
Normal file
1
dictation_client/src/assets/images/shuffle.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="_レイヤー_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 37.17"><defs><style>.cls-1{fill:#282828;}.cls-1,.cls-2{stroke-width:0px;}.cls-2{fill:#e6e6e6;}</style></defs><path class="cls-2" d="M42.13,35.07l-2.15,2.1L3,5.1v6.1H0V0h11.15v3h-6l36.98,32.07Z"/><path class="cls-1" d="M39.98,37.17l-2.1-2.15L74.9,3h-6.1V0h11.2v11.15h-3v-6l-37.03,32.02Z"/></svg>
|
||||
|
After Width: | Height: | Size: 409 B |
1
dictation_client/src/assets/images/upload.svg
Normal file
1
dictation_client/src/assets/images/upload.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M450-313v-371L330-564l-43-43 193-193 193 193-43 43-120-120v371h-60ZM220-160q-24 0-42-18t-18-42v-143h60v143h520v-143h60v143q0 24-18 42t-42 18H220Z"/></svg>
|
||||
|
After Width: | Height: | Size: 251 B |
@ -63,4 +63,26 @@ export const errorCodes = [
|
||||
"E011004", // ワークタイプ使用中エラー
|
||||
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
"E013002", // ワークフロー不在エラー
|
||||
"E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった)
|
||||
"E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)
|
||||
"E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた)
|
||||
"E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた)
|
||||
"E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた)
|
||||
"E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている)
|
||||
"E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた)
|
||||
"E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている)
|
||||
"E015001", // タイピストグループ削除済みエラー
|
||||
"E015002", // タイピストグループがワークフローに紐づいているエラー
|
||||
"E015003", // タイピストグループがルーティングされているエラー
|
||||
"E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
|
||||
"E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた)
|
||||
"E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
|
||||
"E017001", // 親アカウント変更不可エラー(指定したアカウントが存在しない)
|
||||
"E017002", // 親アカウント変更不可エラー(階層関係が不正)
|
||||
"E017003", // 親アカウント変更不可エラー(リージョンが同一でない)
|
||||
"E018001", // パートナーアカウント削除エラー(削除条件を満たしていない)
|
||||
"E019001", // パートナーアカウント取得不可エラー(階層構造が不正)
|
||||
"E020001", // パートナーアカウント変更エラー(変更条件を満たしていない)
|
||||
"E021001", // 音声ファイル名変更不可エラー(権限不足)
|
||||
"E021002", // 音声ファイル名変更不可エラー(同名ファイルが存在)
|
||||
] as const;
|
||||
|
||||
@ -6,4 +6,4 @@ export type ErrorObject = {
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
export type ErrorCodeType = typeof errorCodes[number];
|
||||
export type ErrorCodeType = (typeof errorCodes)[number];
|
||||
|
||||
153
dictation_client/src/common/parser.test.ts
Normal file
153
dictation_client/src/common/parser.test.ts
Normal file
@ -0,0 +1,153 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
// Jestによるparser.tsのテスト
|
||||
import fs from "fs";
|
||||
import { CSVType, parseCSV } from "./parser";
|
||||
|
||||
describe("parse", () => {
|
||||
it("指定形式のCSV文字列をパースできる", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_001.csv", "utf-8");
|
||||
const actualData = await parseCSV(text);
|
||||
const expectData: CSVType[] = [
|
||||
{
|
||||
name: "hoge",
|
||||
email: "sample@example.com",
|
||||
role: 1,
|
||||
author_id: "HOGE",
|
||||
auto_assign: 1,
|
||||
notification: 1,
|
||||
encryption: 1,
|
||||
encryption_password: "abcd",
|
||||
prompt: 0,
|
||||
},
|
||||
];
|
||||
expect(actualData).toEqual(expectData);
|
||||
});
|
||||
it("指定形式のヘッダでない場合、例外が送出される | author_id(値がoptionial)がない", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_002.csv", "utf-8");
|
||||
try {
|
||||
await parseCSV(text);
|
||||
fail("例外が発生しませんでした");
|
||||
} catch (e) {
|
||||
expect(e).toEqual(new Error("Invalid CSV format"));
|
||||
}
|
||||
});
|
||||
it("指定形式のヘッダでない場合、例外が送出される | email(値が必須)がない", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_003.csv", "utf-8");
|
||||
try {
|
||||
await parseCSV(text);
|
||||
fail("例外が発生しませんでした");
|
||||
} catch (e) {
|
||||
expect(e).toEqual(new Error("Invalid CSV format"));
|
||||
}
|
||||
});
|
||||
it("指定形式のヘッダでない場合、例外が送出される | emailがスペルミス", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_004.csv", "utf-8");
|
||||
try {
|
||||
await parseCSV(text);
|
||||
fail("例外が発生しませんでした");
|
||||
} catch (e) {
|
||||
expect(e).toEqual(new Error("Invalid CSV format"));
|
||||
}
|
||||
});
|
||||
it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(文字列)はnullとなる", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_005.csv", "utf-8");
|
||||
const actualData = await parseCSV(text);
|
||||
const expectData: CSVType[] = [
|
||||
{
|
||||
name: "hoge",
|
||||
email: "sample@example.com",
|
||||
role: 1,
|
||||
author_id: null,
|
||||
auto_assign: 1,
|
||||
notification: 1,
|
||||
encryption: 1,
|
||||
encryption_password: "abcd",
|
||||
prompt: 0,
|
||||
},
|
||||
];
|
||||
expect(actualData).toEqual(expectData);
|
||||
});
|
||||
it("指定形式のCSV文字列をパースできる | 抜けているパラメータ(数値)はnullとなる", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_006.csv", "utf-8");
|
||||
const actualData = await parseCSV(text);
|
||||
const expectData: CSVType[] = [
|
||||
{
|
||||
name: "hoge",
|
||||
email: "sample@example.com",
|
||||
role: null,
|
||||
author_id: "HOGE",
|
||||
auto_assign: 1,
|
||||
notification: 1,
|
||||
encryption: 1,
|
||||
encryption_password: "abcd",
|
||||
prompt: 0,
|
||||
},
|
||||
];
|
||||
expect(actualData).toEqual(expectData);
|
||||
});
|
||||
it("指定形式のCSV文字列をパースできる | 余計なパラメータがあっても問題はない", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_007.csv", "utf-8");
|
||||
const actualData = await parseCSV(text);
|
||||
const expectData: CSVType[] = [
|
||||
{
|
||||
name: "hoge",
|
||||
email: "sample@example.com",
|
||||
role: 1,
|
||||
author_id: "HOGE",
|
||||
auto_assign: 1,
|
||||
notification: 1,
|
||||
encryption: 1,
|
||||
encryption_password: "abcd",
|
||||
prompt: 0,
|
||||
},
|
||||
{
|
||||
name: "hoge2",
|
||||
email: "sample2@example.com",
|
||||
role: 1,
|
||||
author_id: "HOGE2",
|
||||
auto_assign: 1,
|
||||
notification: 1,
|
||||
encryption: 1,
|
||||
encryption_password: "abcd2",
|
||||
prompt: 0,
|
||||
},
|
||||
];
|
||||
expect(actualData.length).toBe(expectData.length);
|
||||
|
||||
// 余計なパラメータ格納用に __parsed_extra: string[] というプロパティが作られてしまうので、既知のプロパティ毎に比較
|
||||
for (let i = 0; i < actualData.length; i += 1) {
|
||||
const actualValue = actualData[i];
|
||||
const expectValue = expectData[i];
|
||||
expect(actualValue.author_id).toEqual(expectValue.author_id);
|
||||
expect(actualValue.auto_assign).toEqual(expectValue.auto_assign);
|
||||
expect(actualValue.email).toEqual(expectValue.email);
|
||||
expect(actualValue.encryption).toEqual(expectValue.encryption);
|
||||
expect(actualValue.encryption_password).toEqual(
|
||||
expectValue.encryption_password
|
||||
);
|
||||
expect(actualValue.name).toEqual(expectValue.name);
|
||||
expect(actualValue.notification).toEqual(expectValue.notification);
|
||||
expect(actualValue.prompt).toEqual(expectValue.prompt);
|
||||
expect(actualValue.role).toEqual(expectValue.role);
|
||||
}
|
||||
});
|
||||
|
||||
it("author_id,encryption_passwordが数値のみの場合でも、文字列として変換できる", async () => {
|
||||
const text = fs.readFileSync("src/common/test/test_008.csv", "utf-8");
|
||||
const actualData = await parseCSV(text);
|
||||
const expectData: CSVType[] = [
|
||||
{
|
||||
name: "hoge",
|
||||
email: "sample@example.com",
|
||||
role: 1,
|
||||
author_id: "1111",
|
||||
auto_assign: 1,
|
||||
notification: 1,
|
||||
encryption: 1,
|
||||
encryption_password: "222222",
|
||||
prompt: 0,
|
||||
},
|
||||
];
|
||||
expect(actualData).toEqual(expectData);
|
||||
});
|
||||
});
|
||||
74
dictation_client/src/common/parser.ts
Normal file
74
dictation_client/src/common/parser.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import Papa, { ParseResult } from "papaparse";
|
||||
|
||||
export type CSVType = {
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
role: number | null;
|
||||
author_id: string | null;
|
||||
auto_assign: number | null;
|
||||
notification: number;
|
||||
encryption: number | null;
|
||||
encryption_password: string | null;
|
||||
prompt: number | null;
|
||||
};
|
||||
|
||||
// CSVTypeのプロパティ名を文字列の配列で定義する
|
||||
const CSVTypeFields: (keyof CSVType)[] = [
|
||||
"name",
|
||||
"email",
|
||||
"role",
|
||||
"author_id",
|
||||
"auto_assign",
|
||||
"notification",
|
||||
"encryption",
|
||||
"encryption_password",
|
||||
"prompt",
|
||||
];
|
||||
|
||||
// 2つの配列が等しいかどうかを判定する
|
||||
const equals = (lhs: string[], rhs: string[]) => {
|
||||
if (lhs.length !== rhs.length) return false;
|
||||
for (let i = 0; i < lhs.length; i += 1) {
|
||||
if (lhs[i] !== rhs[i]) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/** CSVファイルをCSVType型に変換するパーサー */
|
||||
export const parseCSV = async (csvString: string): Promise<CSVType[]> =>
|
||||
new Promise((resolve, reject) => {
|
||||
Papa.parse<CSVType>(csvString, {
|
||||
download: false,
|
||||
worker: false, // XXX: workerを使うとエラーが発生するためfalseに設定
|
||||
header: true,
|
||||
dynamicTyping: {
|
||||
// author_id, encryption_passwordは数値のみの場合、numberに変換されたくないためdynamicTypingをtrueにしない
|
||||
role: true,
|
||||
auto_assign: true,
|
||||
notification: true,
|
||||
encryption: true,
|
||||
prompt: true,
|
||||
},
|
||||
// dynamicTypingがfalseの場合、空文字をnullに変換できないためtransformを使用する
|
||||
transform: (value, field) => {
|
||||
if (field === "author_id" || field === "encryption_password") {
|
||||
// 空文字の場合はnullに変換する
|
||||
if (value === "") {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
},
|
||||
complete: (results: ParseResult<CSVType>) => {
|
||||
// ヘッダーがCSVTypeFieldsと一致しない場合はエラーを返す
|
||||
if (!equals(results.meta.fields ?? [], CSVTypeFields)) {
|
||||
reject(new Error("Invalid CSV format"));
|
||||
}
|
||||
resolve(results.data);
|
||||
},
|
||||
error: (error: Error) => {
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
2
dictation_client/src/common/test/test_001.csv
Normal file
2
dictation_client/src/common/test/test_001.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
|
||||
|
2
dictation_client/src/common/test/test_002.csv
Normal file
2
dictation_client/src/common/test/test_002.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,email,role,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
2
dictation_client/src/common/test/test_003.csv
Normal file
2
dictation_client/src/common/test/test_003.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
2
dictation_client/src/common/test/test_004.csv
Normal file
2
dictation_client/src/common/test/test_004.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,emeil,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0
|
||||
|
2
dictation_client/src/common/test/test_005.csv
Normal file
2
dictation_client/src/common/test/test_005.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,,1,1,1,abcd,0
|
||||
|
2
dictation_client/src/common/test/test_006.csv
Normal file
2
dictation_client/src/common/test/test_006.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,,"HOGE",1,1,1,abcd,0
|
||||
|
3
dictation_client/src/common/test/test_007.csv
Normal file
3
dictation_client/src/common/test/test_007.csv
Normal file
@ -0,0 +1,3 @@
|
||||
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,"HOGE",1,1,1,abcd,0,x
|
||||
hoge2,sample2@example.com,1,"HOGE2",1,1,1,abcd2,0,1,32,4,aa
|
||||
|
Can't render this file because it has a wrong number of fields in line 2.
|
2
dictation_client/src/common/test/test_008.csv
Normal file
2
dictation_client/src/common/test/test_008.csv
Normal file
@ -0,0 +1,2 @@
|
||||
name,email,role,author_id,auto_assign,notification,encryption,encryption_password,prompt
|
||||
hoge,sample@example.com,1,1111,1,1,1,222222,0
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
updateAccountInfoAsync,
|
||||
getAccountRelationsAsync,
|
||||
deleteAccountAsync,
|
||||
updateFileDeleteSettingAsync,
|
||||
} from "./operations";
|
||||
|
||||
const initialState: AccountState = {
|
||||
@ -15,6 +16,8 @@ const initialState: AccountState = {
|
||||
tier: 0,
|
||||
country: "",
|
||||
delegationPermission: false,
|
||||
autoFileDelete: false,
|
||||
fileRetentionDays: 0,
|
||||
},
|
||||
},
|
||||
dealers: [],
|
||||
@ -29,6 +32,8 @@ const initialState: AccountState = {
|
||||
secondryAdminUserId: undefined,
|
||||
},
|
||||
isLoading: false,
|
||||
autoFileDelete: false,
|
||||
fileRetentionDays: 0,
|
||||
},
|
||||
};
|
||||
|
||||
@ -64,6 +69,20 @@ export const accountSlice = createSlice({
|
||||
const { secondryAdminUserId } = action.payload;
|
||||
state.apps.updateAccountInfo.secondryAdminUserId = secondryAdminUserId;
|
||||
},
|
||||
changeAutoFileDelete: (
|
||||
state,
|
||||
action: PayloadAction<{ autoFileDelete: boolean }>
|
||||
) => {
|
||||
const { autoFileDelete } = action.payload;
|
||||
state.apps.autoFileDelete = autoFileDelete;
|
||||
},
|
||||
changeFileRetentionDays: (
|
||||
state,
|
||||
action: PayloadAction<{ fileRetentionDays: number }>
|
||||
) => {
|
||||
const { fileRetentionDays } = action.payload;
|
||||
state.apps.fileRetentionDays = fileRetentionDays;
|
||||
},
|
||||
cleanupApps: (state) => {
|
||||
state.domain = initialState.domain;
|
||||
},
|
||||
@ -85,6 +104,10 @@ export const accountSlice = createSlice({
|
||||
action.payload.accountInfo.account.primaryAdminUserId;
|
||||
state.apps.updateAccountInfo.secondryAdminUserId =
|
||||
action.payload.accountInfo.account.secondryAdminUserId;
|
||||
state.apps.autoFileDelete =
|
||||
action.payload.accountInfo.account.autoFileDelete;
|
||||
state.apps.fileRetentionDays =
|
||||
action.payload.accountInfo.account.fileRetentionDays;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getAccountRelationsAsync.rejected, (state) => {
|
||||
@ -99,6 +122,15 @@ export const accountSlice = createSlice({
|
||||
builder.addCase(updateAccountInfoAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(updateFileDeleteSettingAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(updateFileDeleteSettingAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(updateFileDeleteSettingAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteAccountAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
@ -115,6 +147,8 @@ export const {
|
||||
changeDealerPermission,
|
||||
changePrimaryAdministrator,
|
||||
changeSecondryAdministrator,
|
||||
changeAutoFileDelete,
|
||||
changeFileRetentionDays,
|
||||
cleanupApps,
|
||||
} = accountSlice.actions;
|
||||
export default accountSlice.reducer;
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
UpdateAccountInfoRequest,
|
||||
UsersApi,
|
||||
DeleteAccountRequest,
|
||||
UpdateFileDeleteSettingRequest,
|
||||
} from "../../api/api";
|
||||
import { Configuration } from "../../api/configuration";
|
||||
import { ViewAccountRelationsInfo } from "./types";
|
||||
@ -112,6 +113,58 @@ export const updateAccountInfoAsync = createAsyncThunk<
|
||||
}
|
||||
});
|
||||
|
||||
export const updateFileDeleteSettingAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{ autoFileDelete: boolean; fileRetentionDays: number },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("accounts/updateFileDeleteSettingAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountApi = new AccountsApi(config);
|
||||
|
||||
const requestParam: UpdateFileDeleteSettingRequest = {
|
||||
autoFileDelete: args.autoFileDelete,
|
||||
retentionDays: args.fileRetentionDays,
|
||||
};
|
||||
|
||||
try {
|
||||
await accountApi.updateFileDeleteSetting(requestParam, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
|
||||
const errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteAccountAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
|
||||
@ -16,3 +16,18 @@ export const selectIsLoading = (state: RootState) =>
|
||||
state.account.apps.isLoading;
|
||||
export const selectUpdateAccountInfo = (state: RootState) =>
|
||||
state.account.apps.updateAccountInfo;
|
||||
export const selectFileDeleteSetting = (state: RootState) => {
|
||||
const { autoFileDelete, fileRetentionDays } = state.account.apps;
|
||||
return {
|
||||
autoFileDelete,
|
||||
fileRetentionDays,
|
||||
};
|
||||
};
|
||||
export const selectInputValidationErrors = (state: RootState) => {
|
||||
const { fileRetentionDays } = state.account.apps;
|
||||
const hasFileRetentionDaysError =
|
||||
fileRetentionDays <= 0 || fileRetentionDays >= 1000;
|
||||
return {
|
||||
hasFileRetentionDaysError,
|
||||
};
|
||||
};
|
||||
|
||||
@ -19,4 +19,6 @@ export interface Domain {
|
||||
export interface Apps {
|
||||
updateAccountInfo: UpdateAccountInfoRequest;
|
||||
isLoading: boolean;
|
||||
autoFileDelete: boolean;
|
||||
fileRetentionDays: number;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export const STATUS = {
|
||||
BACKUP: "Backup",
|
||||
} as const;
|
||||
|
||||
export type StatusType = typeof STATUS[keyof typeof STATUS];
|
||||
export type StatusType = (typeof STATUS)[keyof typeof STATUS];
|
||||
|
||||
export const LIMIT_TASK_NUM = 100;
|
||||
|
||||
@ -26,7 +26,7 @@ export const SORTABLE_COLUMN = {
|
||||
TranscriptionFinishedDate: "TRANSCRIPTION_FINISHED_DATE",
|
||||
} as const;
|
||||
export type SortableColumnType =
|
||||
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
|
||||
(typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
|
||||
|
||||
export const isSortableColumnType = (
|
||||
value: string
|
||||
@ -36,14 +36,14 @@ export const isSortableColumnType = (
|
||||
};
|
||||
|
||||
export type SortableColumnList =
|
||||
typeof SORTABLE_COLUMN[keyof typeof SORTABLE_COLUMN];
|
||||
(typeof SORTABLE_COLUMN)[keyof typeof SORTABLE_COLUMN];
|
||||
|
||||
export const DIRECTION = {
|
||||
ASC: "ASC",
|
||||
DESC: "DESC",
|
||||
} as const;
|
||||
|
||||
export type DirectionType = typeof DIRECTION[keyof typeof DIRECTION];
|
||||
export type DirectionType = (typeof DIRECTION)[keyof typeof DIRECTION];
|
||||
|
||||
// DirectionTypeの型チェック関数
|
||||
export const isDirectionType = (arg: string): arg is DirectionType =>
|
||||
|
||||
@ -11,6 +11,8 @@ import {
|
||||
playbackAsync,
|
||||
updateAssigneeAsync,
|
||||
cancelAsync,
|
||||
deleteTaskAsync,
|
||||
renameFileAsync,
|
||||
} from "./operations";
|
||||
import {
|
||||
SORTABLE_COLUMN,
|
||||
@ -218,6 +220,25 @@ export const dictationSlice = createSlice({
|
||||
builder.addCase(backupTasksAsync.rejected, (state) => {
|
||||
state.apps.isDownloading = false;
|
||||
});
|
||||
builder.addCase(deleteTaskAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(deleteTaskAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteTaskAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
|
||||
builder.addCase(renameFileAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(renameFileAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(renameFileAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -565,10 +565,21 @@ export const backupTasksAsync = createAsyncThunk<
|
||||
a.click();
|
||||
a.parentNode?.removeChild(a);
|
||||
|
||||
// バックアップ済みに更新
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await tasksApi.backup(task.audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
if (error.code === "E010603") {
|
||||
// タスクが削除済みの場合は成功扱いとする
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -580,8 +591,22 @@ export const backupTasksAsync = createAsyncThunk<
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
if (error.code === "E010603") {
|
||||
// 存在しない音声ファイルをダウンロードしようとした場合
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID(
|
||||
"dictationPage.message.fileAlreadyDeletedError"
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
@ -592,3 +617,143 @@ export const backupTasksAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteTaskAsync = createAsyncThunk<
|
||||
{
|
||||
// empty
|
||||
},
|
||||
{
|
||||
// パラメータ
|
||||
audioFileId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/deleteTaskAsync", async (args, thunkApi) => {
|
||||
const { audioFileId } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const tasksApi = new TasksApi(config);
|
||||
|
||||
try {
|
||||
await tasksApi.deleteTask(audioFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let message = getTranslationID("common.message.internalServerError");
|
||||
|
||||
if (error.statusCode === 400) {
|
||||
if (error.code === "E010603") {
|
||||
// タスクが削除済みの場合は成功扱いとする
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (error.code === "E010601") {
|
||||
// タスクがInprogressの場合はエラー
|
||||
message = getTranslationID("dictationPage.message.deleteFailedError");
|
||||
}
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const renameFileAsync = createAsyncThunk<
|
||||
{
|
||||
// empty
|
||||
},
|
||||
{
|
||||
// パラメータ
|
||||
audioFileId: number;
|
||||
fileName: string;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("dictations/renameFileAsync", async (args, thunkApi) => {
|
||||
const { audioFileId, fileName } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const filesApi = new FilesApi(config);
|
||||
|
||||
try {
|
||||
await filesApi.fileRename(
|
||||
{ fileName, audioFileId },
|
||||
{ headers: { authorization: `Bearer ${accessToken}` } }
|
||||
);
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let message = getTranslationID("common.message.internalServerError");
|
||||
|
||||
// 変更権限がない場合はエラー
|
||||
if (error.code === "E021001") {
|
||||
message = getTranslationID("dictationPage.message.fileRenameFailedError");
|
||||
}
|
||||
|
||||
// ファイル名が既に存在する場合はエラー
|
||||
if (error.code === "E021002") {
|
||||
message = getTranslationID(
|
||||
"dictationPage.message.fileNameAleadyExistsError"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LicenseCardActivateState } from "./state";
|
||||
import { activateCardLicenseAsync } from "./operations";
|
||||
|
||||
const initialState: LicenseCardActivateState = {
|
||||
apps: {
|
||||
@ -14,6 +15,17 @@ export const licenseCardActivateSlice = createSlice({
|
||||
state.apps = initialState.apps;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(activateCardLicenseAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(activateCardLicenseAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(activateCardLicenseAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { cleanupApps } = licenseCardActivateSlice.actions;
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { LicenseSummaryState } from "./state";
|
||||
import { getCompanyNameAsync, getLicenseSummaryAsync } from "./operations";
|
||||
import {
|
||||
getCompanyNameAsync,
|
||||
getLicenseSummaryAsync,
|
||||
updateRestrictionStatusAsync,
|
||||
} from "./operations";
|
||||
|
||||
const initialState: LicenseSummaryState = {
|
||||
domain: {
|
||||
@ -35,12 +39,30 @@ export const licenseSummarySlice = createSlice({
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getLicenseSummaryAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(getLicenseSummaryAsync.fulfilled, (state, action) => {
|
||||
state.domain.licenseSummaryInfo = action.payload;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getLicenseSummaryAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
// 画面側ではgetLicenseSummaryAsyncと並行して呼び出されているため、レーシングを考慮してこちらではisLoadingを更新しない
|
||||
// 本来は両方の完了を待ってからisLoadingを更新するべきだが、現時点ではスピード重視のためケアしない。
|
||||
builder.addCase(getCompanyNameAsync.fulfilled, (state, action) => {
|
||||
state.domain.accountInfo.companyName = action.payload.companyName;
|
||||
});
|
||||
builder.addCase(updateRestrictionStatusAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(updateRestrictionStatusAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(updateRestrictionStatusAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
GetCompanyNameResponse,
|
||||
GetLicenseSummaryResponse,
|
||||
PartnerLicenseInfo,
|
||||
UpdateRestrictionStatusRequest,
|
||||
} from "../../../api/api";
|
||||
import { Configuration } from "../../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../../common/errors";
|
||||
@ -123,3 +124,58 @@ export const getCompanyNameAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const updateRestrictionStatusAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
accountId: number;
|
||||
restricted: boolean;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("accounts/updateRestrictionStatusAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountApi = new AccountsApi(config);
|
||||
|
||||
const requestParam: UpdateRestrictionStatusRequest = {
|
||||
accountId: args.accountId,
|
||||
restricted: args.restricted,
|
||||
};
|
||||
|
||||
try {
|
||||
await accountApi.updateRestrictionStatus(requestParam, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
|
||||
// このAPIでは個別のエラーメッセージは不要
|
||||
const errorMessage = getTranslationID("common.message.internalServerError");
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { RootState } from "app/store";
|
||||
|
||||
// 各値はそのまま画面に表示するので、licenseSummaryInfoとして値を取得する
|
||||
export const selecLicenseSummaryInfo = (state: RootState) =>
|
||||
export const selectLicenseSummaryInfo = (state: RootState) =>
|
||||
state.licenseSummary.domain.licenseSummaryInfo;
|
||||
|
||||
export const selectCompanyName = (state: RootState) =>
|
||||
state.licenseSummary.domain.accountInfo.companyName;
|
||||
|
||||
export const selectIsLoading = (state: RootState) => state.license;
|
||||
export const selectIsLoading = (state: RootState) =>
|
||||
state.licenseSummary.apps.isLoading;
|
||||
|
||||
@ -105,3 +105,82 @@ export const getPartnerLicenseAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const switchParentAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
// パラメータ
|
||||
to: number;
|
||||
children: number[];
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("accounts/switchParentAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
|
||||
const { to, children } = args;
|
||||
|
||||
try {
|
||||
await accountsApi.switchParent(
|
||||
{
|
||||
to,
|
||||
children,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
// TODO:エラー処理
|
||||
if (error.code === "E017001") {
|
||||
errorMessage = getTranslationID(
|
||||
"changeOwnerPopup.message.accountNotFoundError"
|
||||
);
|
||||
}
|
||||
|
||||
if (error.code === "E017002") {
|
||||
errorMessage = getTranslationID(
|
||||
"changeOwnerPopup.message.hierarchyMismatchError"
|
||||
);
|
||||
}
|
||||
|
||||
if (error.code === "E017003") {
|
||||
errorMessage = getTranslationID(
|
||||
"changeOwnerPopup.message.regionMismatchError"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { PartnerLicenseInfo } from "api";
|
||||
import { PartnerLicensesState, HierarchicalElement } from "./state";
|
||||
import { getMyAccountAsync, getPartnerLicenseAsync } from "./operations";
|
||||
import {
|
||||
getMyAccountAsync,
|
||||
getPartnerLicenseAsync,
|
||||
switchParentAsync,
|
||||
} from "./operations";
|
||||
import { ACCOUNTS_VIEW_LIMIT } from "./constants";
|
||||
|
||||
const initialState: PartnerLicensesState = {
|
||||
@ -12,6 +16,8 @@ const initialState: PartnerLicensesState = {
|
||||
tier: 0,
|
||||
country: "",
|
||||
delegationPermission: false,
|
||||
autoFileDelete: false,
|
||||
fileRetentionDays: 0,
|
||||
},
|
||||
total: 0,
|
||||
ownPartnerLicense: {
|
||||
@ -107,6 +113,15 @@ export const partnerLicenseSlice = createSlice({
|
||||
builder.addCase(getPartnerLicenseAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(switchParentAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(switchParentAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(switchParentAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
export const {
|
||||
|
||||
@ -8,6 +8,8 @@ import {
|
||||
AccountsApi,
|
||||
CreatePartnerAccountRequest,
|
||||
GetPartnersResponse,
|
||||
DeletePartnerAccountRequest,
|
||||
GetPartnerUsersResponse,
|
||||
} from "../../api/api";
|
||||
import { Configuration } from "../../api/configuration";
|
||||
|
||||
@ -116,3 +118,170 @@ export const getPartnerInfoAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
// パートナーアカウント削除
|
||||
export const deletePartnerAccountAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
// パラメータ
|
||||
accountId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("partner/deletePartnerAccountAsync", async (args, thunkApi) => {
|
||||
const { accountId } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
const deletePartnerAccountRequest: DeletePartnerAccountRequest = {
|
||||
targetAccountId: accountId,
|
||||
};
|
||||
await accountApi.deletePartnerAccount(deletePartnerAccountRequest, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
if (error.code === "E018001") {
|
||||
errorMessage = getTranslationID(
|
||||
"partnerPage.message.partnerDeleteFailedError"
|
||||
);
|
||||
}
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
// パートナーアカウントユーザー取得
|
||||
export const getPartnerUsersAsync = createAsyncThunk<
|
||||
GetPartnerUsersResponse,
|
||||
{
|
||||
// パラメータ
|
||||
accountId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("partner/getPartnerUsersAsync", async (args, thunkApi) => {
|
||||
const { accountId } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
const res = await accountApi.getPartnerUsers(
|
||||
{ targetAccountId: accountId },
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
|
||||
return res.data;
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
// パートナーアカウントユーザー編集
|
||||
export const editPartnerInfoAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("partner/editPartnerInfoAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountApi = new AccountsApi(config);
|
||||
|
||||
const { id, companyName, selectedAdminId } = state.partner.apps.editPartner;
|
||||
|
||||
try {
|
||||
await accountApi.updatePartnerInfo(
|
||||
{
|
||||
targetAccountId: id,
|
||||
primaryAdminUserId: selectedAdminId,
|
||||
companyName,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
|
||||
return {};
|
||||
} catch (e) {
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
if (error.code === "E010502" || error.code === "E020001") {
|
||||
errorMessage = getTranslationID("partnerPage.message.editFailedError");
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { PartnerState } from "./state";
|
||||
import { createPartnerAccountAsync, getPartnerInfoAsync } from "./operations";
|
||||
import {
|
||||
createPartnerAccountAsync,
|
||||
getPartnerInfoAsync,
|
||||
deletePartnerAccountAsync,
|
||||
getPartnerUsersAsync,
|
||||
editPartnerInfoAsync,
|
||||
} from "./operations";
|
||||
import { LIMIT_PARTNER_VIEW_NUM } from "./constants";
|
||||
|
||||
const initialState: PartnerState = {
|
||||
@ -17,6 +23,13 @@ const initialState: PartnerState = {
|
||||
adminName: "",
|
||||
email: "",
|
||||
},
|
||||
editPartner: {
|
||||
users: [],
|
||||
id: 0,
|
||||
companyName: "",
|
||||
country: "",
|
||||
selectedAdminId: 0,
|
||||
},
|
||||
limit: LIMIT_PARTNER_VIEW_NUM,
|
||||
offset: 0,
|
||||
isLoading: false,
|
||||
@ -75,6 +88,37 @@ export const partnerSlice = createSlice({
|
||||
state.apps.delegatedAccountId = undefined;
|
||||
state.apps.delegatedCompanyName = undefined;
|
||||
},
|
||||
changeEditPartner: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: number;
|
||||
companyName: string;
|
||||
country: string;
|
||||
}>
|
||||
) => {
|
||||
const { id, companyName, country } = action.payload;
|
||||
|
||||
state.apps.editPartner.id = id;
|
||||
state.apps.editPartner.companyName = companyName;
|
||||
state.apps.editPartner.country = country;
|
||||
},
|
||||
changeEditCompanyName: (
|
||||
state,
|
||||
action: PayloadAction<{ companyName: string }>
|
||||
) => {
|
||||
const { companyName } = action.payload;
|
||||
state.apps.editPartner.companyName = companyName;
|
||||
},
|
||||
changeSelectedAdminId: (
|
||||
state,
|
||||
action: PayloadAction<{ adminId: number }>
|
||||
) => {
|
||||
const { adminId } = action.payload;
|
||||
state.apps.editPartner.selectedAdminId = adminId;
|
||||
},
|
||||
cleanupPartnerAccount: (state) => {
|
||||
state.apps.editPartner = initialState.apps.editPartner;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(createPartnerAccountAsync.pending, (state) => {
|
||||
@ -97,6 +141,37 @@ export const partnerSlice = createSlice({
|
||||
builder.addCase(getPartnerInfoAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deletePartnerAccountAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(deletePartnerAccountAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deletePartnerAccountAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getPartnerUsersAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(getPartnerUsersAsync.fulfilled, (state, action) => {
|
||||
const { users } = action.payload;
|
||||
state.apps.editPartner.users = users;
|
||||
state.apps.editPartner.selectedAdminId =
|
||||
users.find((user) => user.isPrimaryAdmin)?.id ?? 0;
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(getPartnerUsersAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(editPartnerInfoAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(editPartnerInfoAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(editPartnerInfoAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
export const {
|
||||
@ -108,5 +183,9 @@ export const {
|
||||
savePageInfo,
|
||||
changeDelegateAccount,
|
||||
cleanupDelegateAccount,
|
||||
changeEditPartner,
|
||||
changeEditCompanyName,
|
||||
changeSelectedAdminId,
|
||||
cleanupPartnerAccount,
|
||||
} = partnerSlice.actions;
|
||||
export default partnerSlice.reducer;
|
||||
|
||||
@ -62,3 +62,17 @@ export const selectDelegatedAccountId = (state: RootState) =>
|
||||
state.partner.apps.delegatedAccountId;
|
||||
export const selectDelegatedCompanyName = (state: RootState) =>
|
||||
state.partner.apps.delegatedCompanyName;
|
||||
|
||||
// edit
|
||||
export const selectEditPartnerId = (state: RootState) =>
|
||||
state.partner.apps.editPartner.id;
|
||||
export const selectEditPartnerCompanyName = (state: RootState) =>
|
||||
state.partner.apps.editPartner.companyName;
|
||||
export const selectEditPartnerCountry = (state: RootState) =>
|
||||
state.partner.apps.editPartner.country;
|
||||
|
||||
export const selectEditPartnerUsers = (state: RootState) =>
|
||||
state.partner.apps.editPartner.users;
|
||||
|
||||
export const selectSelectedAdminId = (state: RootState) =>
|
||||
state.partner.apps.editPartner.selectedAdminId;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
CreatePartnerAccountRequest,
|
||||
GetPartnersResponse,
|
||||
PartnerUser,
|
||||
} from "../../api/api";
|
||||
|
||||
export interface PartnerState {
|
||||
@ -19,4 +20,11 @@ export interface Apps {
|
||||
isLoading: boolean;
|
||||
delegatedAccountId?: number;
|
||||
delegatedCompanyName?: string;
|
||||
editPartner: {
|
||||
users: PartnerUser[];
|
||||
id: number;
|
||||
companyName: string;
|
||||
country: string;
|
||||
selectedAdminId: number;
|
||||
};
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
UsersApi,
|
||||
LicensesApi,
|
||||
GetAllocatableLicensesResponse,
|
||||
MultipleImportUser,
|
||||
} from "../../api/api";
|
||||
import { Configuration } from "../../api/configuration";
|
||||
import { ErrorObject, createErrorObject } from "../../common/errors";
|
||||
@ -383,3 +384,189 @@ export const deallocateLicenseAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteUserAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
// 引数
|
||||
{
|
||||
userId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("users/deleteUserAsync", async (args, thunkApi) => {
|
||||
const { userId } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
await usersApi.deleteUser(
|
||||
{
|
||||
userId,
|
||||
},
|
||||
{
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
}
|
||||
);
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
|
||||
let errorMessage = getTranslationID("common.message.internalServerError");
|
||||
|
||||
if (error.statusCode === 400) {
|
||||
if (error.code === "E014001") {
|
||||
// ユーザーが削除済みのため成功
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ユーザーに有効なライセンスが割り当たっているため削除不可
|
||||
if (error.code === "E014007") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.userDeletionLicenseActiveError"
|
||||
);
|
||||
}
|
||||
// 管理者ユーザーため削除不可
|
||||
if (error.code === "E014002") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.adminUserDeletionError"
|
||||
);
|
||||
}
|
||||
// タイピストユーザーで担当タスクがあるため削除不可
|
||||
if (error.code === "E014009") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.typistUserDeletionTranscriptionTaskError"
|
||||
);
|
||||
}
|
||||
// タイピストユーザーでルーティングルールに設定されているため削除不可
|
||||
if (error.code === "E014004") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.typistDeletionRoutingRuleError"
|
||||
);
|
||||
}
|
||||
// タイピストユーザーでTranscriptionistGroupに所属しているため削除不可
|
||||
if (error.code === "E014005") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.typistUserDeletionTranscriptionistGroupError"
|
||||
);
|
||||
}
|
||||
// Authorユーザーで同一AuthorIDのタスクがあるため削除不可
|
||||
if (error.code === "E014006") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.authorUserDeletionTranscriptionTaskError"
|
||||
);
|
||||
}
|
||||
// Authorユーザーで同一AuthorIDがルーティングルールに設定されているため削除不可
|
||||
if (error.code === "E014003") {
|
||||
errorMessage = getTranslationID(
|
||||
"userListPage.message.authorDeletionRoutingRuleError"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: errorMessage,
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const importUsersAsync = createAsyncThunk<
|
||||
// 正常時の戻り値の型
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
// 引数
|
||||
void,
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("users/importUsersAsync", async (args, thunkApi) => {
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const { importFileName, importUsers } = state.user.apps;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const usersApi = new UsersApi(config);
|
||||
|
||||
try {
|
||||
if (importFileName === undefined) {
|
||||
throw new Error("importFileName is undefined");
|
||||
}
|
||||
|
||||
// CSVデータをAPIに送信するためのデータに変換
|
||||
const users: MultipleImportUser[] = importUsers.map((user) => ({
|
||||
name: user.name ?? "",
|
||||
email: user.email ?? "",
|
||||
role: user.role ?? 0,
|
||||
authorId: user.author_id ?? undefined,
|
||||
autoRenew: user.auto_assign ?? 0,
|
||||
notification: user.notification ?? 0,
|
||||
encryption: user.encryption ?? undefined,
|
||||
encryptionPassword: user.encryption_password ?? undefined,
|
||||
prompt: user.prompt ?? undefined,
|
||||
}));
|
||||
|
||||
await usersApi.multipleImports(
|
||||
{
|
||||
filename: importFileName,
|
||||
users,
|
||||
},
|
||||
{ headers: { authorization: `Bearer ${accessToken}` } }
|
||||
);
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("userListPage.message.importSuccess"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換
|
||||
const error = createErrorObject(e);
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message: getTranslationID("common.message.internalServerError"),
|
||||
})
|
||||
);
|
||||
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -382,3 +382,142 @@ const convertValueBasedOnLicenseStatus = (
|
||||
remaining: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const selectImportFileName = (state: RootState) =>
|
||||
state.user.apps.importFileName;
|
||||
|
||||
export const selectImportValidationErrors = (state: RootState) => {
|
||||
const csvUsers = state.user.apps.importUsers;
|
||||
|
||||
let rowNumber = 1;
|
||||
const invalidInput: number[] = [];
|
||||
|
||||
const duplicatedEmailsMap = new Map<string, number>();
|
||||
const duplicatedAuthorIdsMap = new Map<string, number>();
|
||||
const overMaxRow = csvUsers.length > 100;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const csvUser of csvUsers) {
|
||||
rowNumber += 1;
|
||||
|
||||
// メールアドレスの重複がある場合、エラーとしてその行番号を追加する
|
||||
const duplicatedEmailUser = csvUsers.filter(
|
||||
(x) => x.email === csvUser.email
|
||||
);
|
||||
if (duplicatedEmailUser.length > 1) {
|
||||
if (csvUser.email !== null && !duplicatedEmailsMap.has(csvUser.email)) {
|
||||
duplicatedEmailsMap.set(csvUser.email, rowNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// AuthorIDの重複がある場合、エラーとしてその行番号を追加する
|
||||
const duplicatedAuthorIdUser = csvUsers.filter(
|
||||
(x) => x.author_id === csvUser.author_id
|
||||
);
|
||||
if (duplicatedAuthorIdUser.length > 1) {
|
||||
if (
|
||||
csvUser.author_id !== null &&
|
||||
!duplicatedAuthorIdsMap.has(csvUser.author_id)
|
||||
) {
|
||||
duplicatedAuthorIdsMap.set(csvUser.author_id, rowNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// name
|
||||
if (csvUser.name === null || csvUser.name.length > 225) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// email
|
||||
const emailPattern =
|
||||
/^[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]+@[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*\.[a-zA-Z0-9!#$%&'_`/=~+\-?^{|}.]*[a-zA-Z]$/;
|
||||
if (
|
||||
csvUser.name === null ||
|
||||
csvUser.name.length > 225 ||
|
||||
!emailPattern.test(csvUser.email ?? "")
|
||||
) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// role
|
||||
if (csvUser.role === null || ![0, 1, 2].includes(csvUser.role)) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// role=1(Author)
|
||||
if (csvUser.role === 1) {
|
||||
// author_id
|
||||
if (csvUser.author_id === null || csvUser.author_id.length > 16) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
// 半角英数字と_の組み合わせで16文字まで
|
||||
const charaTypePattern = /^[A-Z0-9_]{1,16}$/;
|
||||
const charaType = new RegExp(charaTypePattern).test(csvUser.author_id);
|
||||
if (!charaType) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// encryption
|
||||
if (csvUser.encryption === null || ![0, 1].includes(csvUser.encryption)) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
if (csvUser.encryption === 1) {
|
||||
// encryption_password
|
||||
if (csvUser.encryption === 1) {
|
||||
const regex = /^[!-~]{4,16}$/;
|
||||
if (!regex.test(csvUser.encryption_password ?? "")) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prompt
|
||||
if (csvUser.prompt === null || ![0, 1].includes(csvUser.prompt)) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// auto_assign
|
||||
if (csvUser.auto_assign === null || ![0, 1].includes(csvUser.auto_assign)) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// notification
|
||||
if (
|
||||
csvUser.notification === null ||
|
||||
![0, 1].includes(csvUser.notification)
|
||||
) {
|
||||
invalidInput.push(rowNumber);
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const duplicatedEmails = Array.from(duplicatedEmailsMap.values());
|
||||
const duplicatedAuthorIds = Array.from(duplicatedAuthorIdsMap.values());
|
||||
|
||||
return {
|
||||
invalidInput,
|
||||
duplicatedEmails,
|
||||
duplicatedAuthorIds,
|
||||
overMaxRow,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { CSVType } from "common/parser";
|
||||
import { User, AllocatableLicenseInfo } from "../../api/api";
|
||||
import { AddUser, UpdateUser, LicenseAllocateUser } from "./types";
|
||||
|
||||
@ -19,4 +20,6 @@ export interface Apps {
|
||||
selectedlicenseId: number;
|
||||
hasPasswordMask: boolean;
|
||||
isLoading: boolean;
|
||||
importFileName: string | undefined;
|
||||
importUsers: CSVType[];
|
||||
}
|
||||
|
||||
@ -54,14 +54,14 @@ export interface LicenseAllocateUser {
|
||||
remaining?: number;
|
||||
}
|
||||
|
||||
export type RoleType = typeof USER_ROLES[keyof typeof USER_ROLES];
|
||||
export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
|
||||
// 受け取った値がUSER_ROLESの型であるかどうかを判定する
|
||||
export const isRoleType = (role: string): role is RoleType =>
|
||||
Object.values(USER_ROLES).includes(role as RoleType);
|
||||
|
||||
export type LicenseStatusType =
|
||||
typeof LICENSE_STATUS[keyof typeof LICENSE_STATUS];
|
||||
(typeof LICENSE_STATUS)[keyof typeof LICENSE_STATUS];
|
||||
|
||||
// 受け取った値がLicenseStatusTypeの型であるかどうかを判定する
|
||||
export const isLicenseStatusType = (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { USER_ROLES } from "components/auth/constants";
|
||||
import { CSVType } from "common/parser";
|
||||
import { UsersState } from "./state";
|
||||
import {
|
||||
addUserAsync,
|
||||
@ -7,6 +8,8 @@ import {
|
||||
updateUserAsync,
|
||||
getAllocatableLicensesAsync,
|
||||
deallocateLicenseAsync,
|
||||
deleteUserAsync,
|
||||
importUsersAsync,
|
||||
} from "./operations";
|
||||
import { RoleType, UserView } from "./types";
|
||||
|
||||
@ -60,6 +63,8 @@ const initialState: UsersState = {
|
||||
selectedlicenseId: 0,
|
||||
hasPasswordMask: false,
|
||||
isLoading: false,
|
||||
importFileName: undefined,
|
||||
importUsers: [],
|
||||
},
|
||||
};
|
||||
|
||||
@ -241,6 +246,21 @@ export const userSlice = createSlice({
|
||||
state.apps.licenseAllocateUser = initialState.apps.licenseAllocateUser;
|
||||
state.apps.selectedlicenseId = initialState.apps.selectedlicenseId;
|
||||
},
|
||||
changeImportFileName: (
|
||||
state,
|
||||
action: PayloadAction<{ fileName: string }>
|
||||
) => {
|
||||
const { fileName } = action.payload;
|
||||
state.apps.importFileName = fileName;
|
||||
},
|
||||
changeImportCsv: (state, action: PayloadAction<{ users: CSVType[] }>) => {
|
||||
const { users } = action.payload;
|
||||
state.apps.importUsers = users;
|
||||
},
|
||||
cleanupImportUsers: (state) => {
|
||||
state.apps.importFileName = initialState.apps.importFileName;
|
||||
state.apps.importUsers = initialState.apps.importUsers;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(listUsersAsync.pending, (state) => {
|
||||
@ -290,6 +310,24 @@ export const userSlice = createSlice({
|
||||
builder.addCase(deallocateLicenseAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteUserAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(deleteUserAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteUserAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(importUsersAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(importUsersAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(importUsersAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -317,6 +355,9 @@ export const {
|
||||
changeLicenseAllocateUser,
|
||||
changeSelectedlicenseId,
|
||||
cleanupLicenseAllocateInfo,
|
||||
changeImportFileName,
|
||||
changeImportCsv,
|
||||
cleanupImportUsers,
|
||||
} = userSlice.actions;
|
||||
|
||||
export default userSlice.reducer;
|
||||
|
||||
@ -115,3 +115,78 @@ export const uploadTemplateAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteTemplateAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{ templateFileId: number },
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("workflow/deleteTemplateAsync", async (args, thunkApi) => {
|
||||
const { templateFileId } = args;
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const templateApi = new TemplatesApi(config);
|
||||
|
||||
try {
|
||||
// ファイルを削除する
|
||||
await templateApi.deleteTemplateFile(templateFileId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
if (error.code === "E016001") {
|
||||
// テンプレートファイルが削除済みの場合は成功扱いとする
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
let message = getTranslationID("common.message.internalServerError");
|
||||
|
||||
// テンプレートファイルがルーティングルールに紐づく場合はエラー
|
||||
if (error.code === "E016002") {
|
||||
message = getTranslationID(
|
||||
"templateFilePage.message.deleteFailedWorkflowAssigned"
|
||||
);
|
||||
}
|
||||
// テンプレートファイルが未完了のタスクに紐づく場合はエラー
|
||||
if (error.code === "E016003") {
|
||||
message = getTranslationID(
|
||||
"templateFilePage.message.deleteFailedTaskAssigned"
|
||||
);
|
||||
}
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "error",
|
||||
message,
|
||||
})
|
||||
);
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
import { TemplateState } from "./state";
|
||||
import { listTemplateAsync, uploadTemplateAsync } from "./operations";
|
||||
import {
|
||||
deleteTemplateAsync,
|
||||
listTemplateAsync,
|
||||
uploadTemplateAsync,
|
||||
} from "./operations";
|
||||
|
||||
const initialState: TemplateState = {
|
||||
apps: {
|
||||
@ -45,6 +49,15 @@ export const templateSlice = createSlice({
|
||||
builder.addCase(uploadTemplateAsync.rejected, (state) => {
|
||||
state.apps.isUploading = false;
|
||||
});
|
||||
builder.addCase(deleteTemplateAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(deleteTemplateAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteTemplateAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -269,3 +269,70 @@ export const updateTypistGroupAsync = createAsyncThunk<
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteTypistGroupAsync = createAsyncThunk<
|
||||
{
|
||||
/* Empty Object */
|
||||
},
|
||||
{
|
||||
typistGroupId: number;
|
||||
},
|
||||
{
|
||||
// rejectした時の返却値の型
|
||||
rejectValue: {
|
||||
error: ErrorObject;
|
||||
};
|
||||
}
|
||||
>("workflow/deleteTypistGroupAsync", async (args, thunkApi) => {
|
||||
const { typistGroupId } = args;
|
||||
|
||||
// apiのConfigurationを取得する
|
||||
const { getState } = thunkApi;
|
||||
const state = getState() as RootState;
|
||||
const { configuration } = state.auth;
|
||||
const accessToken = getAccessToken(state.auth);
|
||||
const config = new Configuration(configuration);
|
||||
const accountsApi = new AccountsApi(config);
|
||||
|
||||
try {
|
||||
await accountsApi.deleteTypistGroup(typistGroupId, {
|
||||
headers: { authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
} catch (e) {
|
||||
// e ⇒ errorObjectに変換"
|
||||
const error = createErrorObject(e);
|
||||
|
||||
// すでに削除されていた場合は成功扱いする
|
||||
if (error.code === "E015001") {
|
||||
thunkApi.dispatch(
|
||||
openSnackbar({
|
||||
level: "info",
|
||||
message: getTranslationID("common.message.success"),
|
||||
})
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
// 以下は実際の削除失敗
|
||||
let message = getTranslationID("common.message.internalServerError");
|
||||
if (error.code === "E015002")
|
||||
message = getTranslationID(
|
||||
"typistGroupSetting.message.deleteFailedWorkflowAssigned"
|
||||
);
|
||||
if (error.code === "E015003")
|
||||
message = getTranslationID(
|
||||
"typistGroupSetting.message.deleteFailedCheckoutPermissionExisted"
|
||||
);
|
||||
|
||||
thunkApi.dispatch(openSnackbar({ level: "error", message }));
|
||||
return thunkApi.rejectWithValue({ error });
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
listTypistGroupsAsync,
|
||||
listTypistsAsync,
|
||||
updateTypistGroupAsync,
|
||||
deleteTypistGroupAsync,
|
||||
} from "./operations";
|
||||
|
||||
const initialState: TypistGroupState = {
|
||||
@ -106,6 +107,15 @@ export const typistGroupSlice = createSlice({
|
||||
builder.addCase(updateTypistGroupAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteTypistGroupAsync.pending, (state) => {
|
||||
state.apps.isLoading = true;
|
||||
});
|
||||
builder.addCase(deleteTypistGroupAsync.fulfilled, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
builder.addCase(deleteTypistGroupAsync.rejected, (state) => {
|
||||
state.apps.isLoading = false;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { OPTION_ITEMS_DEFAULT_VALUE_TYPE } from "./constants";
|
||||
|
||||
// OPTION_ITEMS_DEFAULT_VALUE_TYPEからOptionItemDefaultValueTypeを作成する
|
||||
export type OptionItemsDefaultValueType =
|
||||
typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE];
|
||||
(typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE)[keyof typeof OPTION_ITEMS_DEFAULT_VALUE_TYPE];
|
||||
|
||||
// 受け取った値がOptionItemDefaultValueType型かどうかを判定する
|
||||
export const isOptionItemDefaultValueType = (
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectInputValidationErrors,
|
||||
selectFileDeleteSetting,
|
||||
updateFileDeleteSettingAsync,
|
||||
selectIsLoading,
|
||||
getAccountRelationsAsync,
|
||||
} from "features/account";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styles from "../../styles/app.module.scss";
|
||||
import { getTranslationID } from "../../translation";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import {
|
||||
changeAutoFileDelete,
|
||||
changeFileRetentionDays,
|
||||
} from "../../features/account/accountSlice";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface FileDeleteSettingPopupProps {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const FileDeleteSettingPopup: React.FC<FileDeleteSettingPopupProps> = (
|
||||
props
|
||||
) => {
|
||||
const { onClose } = props;
|
||||
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [t] = useTranslation();
|
||||
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const fileDeleteSetting = useSelector(selectFileDeleteSetting);
|
||||
const { hasFileRetentionDaysError } = useSelector(
|
||||
selectInputValidationErrors
|
||||
);
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) return;
|
||||
onClose();
|
||||
}, [isLoading, onClose]);
|
||||
|
||||
const [isPushSubmitButton, setIsPushSubmitButton] = useState<boolean>(false);
|
||||
|
||||
const onUpdateFileDeleteSetting = useCallback(async () => {
|
||||
if (isLoading) return;
|
||||
setIsPushSubmitButton(true);
|
||||
if (hasFileRetentionDaysError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(
|
||||
updateFileDeleteSettingAsync({
|
||||
autoFileDelete: fileDeleteSetting.autoFileDelete,
|
||||
fileRetentionDays: fileDeleteSetting.fileRetentionDays,
|
||||
})
|
||||
);
|
||||
setIsPushSubmitButton(false);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
closePopup();
|
||||
dispatch(getAccountRelationsAsync());
|
||||
}
|
||||
}, [
|
||||
closePopup,
|
||||
dispatch,
|
||||
fileDeleteSetting.autoFileDelete,
|
||||
fileDeleteSetting.fileRetentionDays,
|
||||
hasFileRetentionDaysError,
|
||||
isLoading,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("fileDeleteSettingPopup.label.title"))}
|
||||
<button type="button" onClick={closePopup}>
|
||||
<img src={close} className={styles.modalTitleIcon} alt="close" />
|
||||
</button>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle} />
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"fileDeleteSettingPopup.label.autoFileDeleteCheck"
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
<dd className={styles.last}>
|
||||
<p>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.formCheck}
|
||||
checked={fileDeleteSetting.autoFileDelete}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeAutoFileDelete({
|
||||
autoFileDelete: e.target.checked,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</p>
|
||||
<p className={styles.txWsline}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"fileDeleteSettingPopup.label.daysAnnotation"
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={999}
|
||||
value={fileDeleteSetting.fileRetentionDays}
|
||||
className={`${styles.formInput} ${styles.short}`}
|
||||
disabled={!fileDeleteSetting.autoFileDelete}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeFileRetentionDays({
|
||||
fileRetentionDays: Number(e.target.value),
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>{" "}
|
||||
{t(getTranslationID("fileDeleteSettingPopup.label.days"))}
|
||||
{isPushSubmitButton && hasFileRetentionDaysError && (
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"fileDeleteSettingPopup.label.daysValidationError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(
|
||||
getTranslationID("fileDeleteSettingPopup.label.saveButton")
|
||||
)}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={onUpdateFileDeleteSetting}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</dd>
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -23,6 +23,7 @@ import { getTranslationID } from "translation";
|
||||
import { TIERS } from "components/auth/constants";
|
||||
import { isApproveTier } from "features/auth";
|
||||
import { DeleteAccountPopup } from "./deleteAccountPopup";
|
||||
import { FileDeleteSettingPopup } from "./fileDeleteSettingPopup";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
const AccountPage: React.FC = (): JSX.Element => {
|
||||
@ -40,10 +41,17 @@ const AccountPage: React.FC = (): JSX.Element => {
|
||||
const [isDeleteAccountPopupOpen, setIsDeleteAccountPopupOpen] =
|
||||
useState(false);
|
||||
|
||||
const [isFileDeleteSettingPopupOpen, setIsFileDeleteSettingPopupOpen] =
|
||||
useState(false);
|
||||
|
||||
const onDeleteAccountOpen = useCallback(() => {
|
||||
setIsDeleteAccountPopupOpen(true);
|
||||
}, [setIsDeleteAccountPopupOpen]);
|
||||
|
||||
const onDeleteFileDeleteSettingOpen = useCallback(() => {
|
||||
setIsFileDeleteSettingPopupOpen(true);
|
||||
}, [setIsFileDeleteSettingPopupOpen]);
|
||||
|
||||
// 階層表示用
|
||||
const tierNames: { [key: number]: string } = {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@ -89,6 +97,13 @@ const AccountPage: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isFileDeleteSettingPopupOpen && (
|
||||
<FileDeleteSettingPopup
|
||||
onClose={() => {
|
||||
setIsFileDeleteSettingPopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
@ -102,12 +117,13 @@ const AccountPage: React.FC = (): JSX.Element => {
|
||||
|
||||
<section className={styles.account}>
|
||||
<div className={styles.boxFlex}>
|
||||
{/* File Delete Setting は現状不要のため非表示
|
||||
<ul className={`${styles.menuAction} ${styles.box100}`}>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
href="account_setting.html"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={onDeleteFileDeleteSettingOpen}
|
||||
data-tag="open-file-delete-setting-popup"
|
||||
>
|
||||
<img
|
||||
src="images/file_delete.svg"
|
||||
@ -120,7 +136,6 @@ const AccountPage: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
*/}
|
||||
|
||||
<div className={styles.marginRgt3}>
|
||||
<dl className={styles.listVertical}>
|
||||
@ -216,9 +231,23 @@ const AccountPage: React.FC = (): JSX.Element => {
|
||||
)
|
||||
)}
|
||||
</dd>
|
||||
<dd
|
||||
style={{ paddingBottom: 0 }}
|
||||
className={`${styles.full}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isTier5 && <dd>-</dd>}
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID("accountPage.label.fileRetentionDays")
|
||||
)}
|
||||
</dt>
|
||||
<dd>
|
||||
{viewInfo.account.autoFileDelete
|
||||
? viewInfo.account.fileRetentionDays
|
||||
: "-"}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import React, { useCallback } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectSelectedFileTask,
|
||||
selectIsLoading,
|
||||
PRIORITY,
|
||||
renameFileAsync,
|
||||
} from "features/dictation";
|
||||
import { getTranslationID } from "translation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AppDispatch } from "app/store";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import lock from "../../assets/images/lock.svg";
|
||||
|
||||
@ -19,14 +21,55 @@ interface FilePropertyPopupProps {
|
||||
export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
|
||||
const { onClose, isOpen } = props;
|
||||
const [t] = useTranslation();
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
const [isPushSaveButton, setIsPushSaveButton] = useState<boolean>(false);
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
const closePopup = useCallback(() => {
|
||||
setIsPushSaveButton(false);
|
||||
onClose(false);
|
||||
}, [onClose]);
|
||||
const selectedFileTask = useSelector(selectSelectedFileTask);
|
||||
|
||||
const [fileName, setFileName] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFileName(selectedFileTask?.fileName ?? "");
|
||||
}
|
||||
}, [selectedFileTask, isOpen]);
|
||||
|
||||
// ファイル名の保存処理
|
||||
const saveFileName = useCallback(async () => {
|
||||
setIsPushSaveButton(true);
|
||||
if (fileName.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ダイアログ確認
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(
|
||||
renameFileAsync({
|
||||
audioFileId: selectedFileTask?.audioFileId ?? 0,
|
||||
fileName,
|
||||
})
|
||||
);
|
||||
|
||||
setIsPushSaveButton(false);
|
||||
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
onClose(true);
|
||||
}
|
||||
}, [t, dispatch, onClose, fileName, selectedFileTask]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||
<div className={styles.modalBox}>
|
||||
@ -45,7 +88,41 @@ export const FilePropertyPopup: React.FC<FilePropertyPopupProps> = (props) => {
|
||||
{t(getTranslationID("filePropertyPopup.label.general"))}
|
||||
</dt>
|
||||
<dt>{t(getTranslationID("dictationPage.label.fileName"))}</dt>
|
||||
<dd>{selectedFileTask?.fileName.replace(".zip", "") ?? ""}</dd>
|
||||
<dd className={styles.hasInput}>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
maxLength={64}
|
||||
value={fileName}
|
||||
className={`${styles.formInput} ${styles.short} ${
|
||||
isPushSaveButton && fileName.length === 0 && styles.isError
|
||||
}`}
|
||||
onChange={(e) => setFileName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(getTranslationID("dictationPage.label.fileNameSave"))}
|
||||
className={`${styles.formSubmit} ${styles.isActive}`}
|
||||
style={{
|
||||
position: "relative",
|
||||
marginTop: "0.2rem",
|
||||
right: "auto",
|
||||
maxWidth: "18rem",
|
||||
whiteSpace: "normal",
|
||||
overflowWrap: "break-word",
|
||||
fontSize: "small",
|
||||
}}
|
||||
onClick={saveFileName}
|
||||
/>
|
||||
{isPushSaveButton && fileName.length === 0 && (
|
||||
<span className={styles.formError}>
|
||||
{t(getTranslationID("common.message.inputEmptyError"))}
|
||||
</span>
|
||||
)}
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("dictationPage.label.rawFileName"))}</dt>
|
||||
<dd>{selectedFileTask?.rawFileName ?? ""}</dd>
|
||||
<dt>{t(getTranslationID("dictationPage.label.fileSize"))}</dt>
|
||||
<dd>{selectedFileTask?.fileSize ?? ""}</dd>
|
||||
<dt>{t(getTranslationID("dictationPage.label.fileLength"))}</dt>
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
playbackAsync,
|
||||
cancelAsync,
|
||||
PRIORITY,
|
||||
deleteTaskAsync,
|
||||
isSortableColumnType,
|
||||
isDirectionType,
|
||||
} from "features/dictation";
|
||||
@ -63,6 +64,8 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
const isTypist = isTypistUser();
|
||||
const isNone = !isAuthor && !isTypist;
|
||||
|
||||
const isDeletableRole = isAdmin || isAuthor;
|
||||
|
||||
// popup制御関係
|
||||
const [
|
||||
isChangeTranscriptionistPopupOpen,
|
||||
@ -496,9 +499,39 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
setIsBackupPopupOpen(true);
|
||||
}, []);
|
||||
|
||||
const onCloseFilePropertyPopup = useCallback(() => {
|
||||
const onCloseFilePropertyPopup = useCallback(
|
||||
(isChanged: boolean) => {
|
||||
if (isChanged) {
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
offset: 0,
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
})
|
||||
);
|
||||
}
|
||||
setIsFilePropertyPopupOpen(false);
|
||||
}, []);
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
]
|
||||
);
|
||||
|
||||
const sortIconClass = (
|
||||
currentParam: SortableColumnType,
|
||||
@ -514,6 +547,53 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
return styles.isActiveAz;
|
||||
};
|
||||
|
||||
const onDeleteTask = useCallback(
|
||||
async (audioFileId: number) => {
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { meta } = await dispatch(
|
||||
deleteTaskAsync({
|
||||
audioFileId,
|
||||
})
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
const filter = getFilter(
|
||||
filterUploaded,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterFinished,
|
||||
filterBackup
|
||||
);
|
||||
dispatch(
|
||||
listTasksAsync({
|
||||
limit: LIMIT_TASK_NUM,
|
||||
offset: 0,
|
||||
filter,
|
||||
direction: sortDirection,
|
||||
paramName: sortableParamName,
|
||||
})
|
||||
);
|
||||
dispatch(listTypistsAsync());
|
||||
dispatch(listTypistGroupsAsync());
|
||||
}
|
||||
},
|
||||
[
|
||||
dispatch,
|
||||
filterBackup,
|
||||
filterFinished,
|
||||
filterInProgress,
|
||||
filterPending,
|
||||
filterUploaded,
|
||||
sortDirection,
|
||||
sortableParamName,
|
||||
t,
|
||||
]
|
||||
);
|
||||
|
||||
// 初回読み込み処理
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@ -1183,9 +1263,19 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
{/* タスク削除はCCB後回し分なので今は非表示
|
||||
<li>
|
||||
<a>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
// タスクのステータスがInprogressまたはPending以外の場合、削除ボタンを活性化する
|
||||
className={
|
||||
isDeletableRole &&
|
||||
x.status !== STATUS.INPROGRESS &&
|
||||
x.status !== STATUS.PENDING
|
||||
? ""
|
||||
: styles.isDisable
|
||||
}
|
||||
onClick={() => onDeleteTask(x.audioFileId)}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"dictationPage.label.deleteDictation"
|
||||
@ -1193,7 +1283,6 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
*/}
|
||||
</ul>
|
||||
</td>
|
||||
{displayColumn.JobNumber && (
|
||||
@ -1252,9 +1341,7 @@ const DictationPage: React.FC = (): JSX.Element => {
|
||||
<td className={styles.clm6}>{x.workType}</td>
|
||||
)}
|
||||
{displayColumn.FileName && (
|
||||
<td className={styles.clm7}>
|
||||
{x.fileName.replace(".zip", "")}
|
||||
</td>
|
||||
<td className={styles.clm7}>{x.fileName}</td>
|
||||
)}
|
||||
{displayColumn.FileLength && (
|
||||
<td className={styles.clm8}>{x.audioDuration}</td>
|
||||
|
||||
203
dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx
Normal file
203
dictation_client/src/pages/LicensePage/changeOwnerPopup.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
selectChildrenPartnerLicenses,
|
||||
selectIsLoading,
|
||||
selectOwnPartnerLicense,
|
||||
} from "features/license/partnerLicense/selectors";
|
||||
import {
|
||||
getMyAccountAsync,
|
||||
switchParentAsync,
|
||||
} from "features/license/partnerLicense/operations";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { clearHierarchicalElement } from "features/license/partnerLicense";
|
||||
import styles from "../../styles/app.module.scss";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import shuffle from "../../assets/images/shuffle.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface ChangeOwnerPopupProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ChangeOwnerPopup: React.FC<ChangeOwnerPopupProps> = (props) => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selectedChildId, setSelectedChildId] = useState<number | null>(null);
|
||||
const [selectedChildName, setSelectedChildName] = useState<string>("");
|
||||
const [destinationParentId, setDestinationParentId] = useState<string>("");
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const originParentLicenseInfo = useSelector(selectOwnPartnerLicense);
|
||||
const childrenLicenseInfos = useSelector(selectChildrenPartnerLicenses);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
const { onClose } = props;
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) return;
|
||||
onClose();
|
||||
}, [isLoading, onClose]);
|
||||
|
||||
const bulkDisplayName = "-- Bulk --";
|
||||
const bulkValue = "bulk";
|
||||
|
||||
const onBulkChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { value } = e.target;
|
||||
const childId = value === bulkValue ? null : Number(value);
|
||||
setSelectedChildId(childId);
|
||||
|
||||
// 一括追加のときは子アカウント名を表示しない
|
||||
let childName = "";
|
||||
if (childId) {
|
||||
const child = childrenLicenseInfos.find((c) => c.accountId === childId);
|
||||
// childがundefinedになることはないが、コード解析対応のためのチェック
|
||||
if (child) {
|
||||
childName = child.companyName;
|
||||
}
|
||||
}
|
||||
setSelectedChildName(childName);
|
||||
},
|
||||
[childrenLicenseInfos]
|
||||
);
|
||||
|
||||
const onSaveClick = useCallback(async () => {
|
||||
const destinationParentIdNum = Number(destinationParentId);
|
||||
if (
|
||||
Number.isNaN(destinationParentIdNum) || // 数値でない場合
|
||||
destinationParentIdNum <= 0 || // IDにならない数値の場合
|
||||
destinationParentId.length > 7 // 8桁以上の場合(本システムの特徴として8桁以上になることはあり得ない)
|
||||
) {
|
||||
setError(t(getTranslationID("changeOwnerPopup.label.invalidInputError")));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const children = selectedChildId
|
||||
? [selectedChildId]
|
||||
: childrenLicenseInfos.map((child) => child.accountId);
|
||||
const { meta } = await dispatch(
|
||||
switchParentAsync({ to: Number(destinationParentId), children })
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(getMyAccountAsync());
|
||||
dispatch(clearHierarchicalElement());
|
||||
closePopup();
|
||||
}
|
||||
}, [
|
||||
childrenLicenseInfos,
|
||||
closePopup,
|
||||
destinationParentId,
|
||||
dispatch,
|
||||
selectedChildId,
|
||||
t,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${styles.isShow}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("changeOwnerPopup.label.title"))}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-noninteractive-element-interactions */}
|
||||
<img
|
||||
src={close}
|
||||
className={styles.modalTitleIcon}
|
||||
alt="close"
|
||||
onClick={closePopup}
|
||||
/>
|
||||
</p>
|
||||
<form action="" name="" method="" className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle} />
|
||||
<dt>
|
||||
{t(getTranslationID("changeOwnerPopup.label.upperLayerId"))}
|
||||
</dt>
|
||||
<dd className={styles.ownerChange}>
|
||||
<p className={styles.Owner}>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={originParentLicenseInfo.accountId}
|
||||
readOnly
|
||||
className={`${styles.formInput} ${styles.short}`}
|
||||
/>
|
||||
<span className={styles.txName}>
|
||||
{originParentLicenseInfo.companyName}
|
||||
</span>
|
||||
</p>
|
||||
<p className={styles.arrowR} />
|
||||
<p className={styles.newOwner}>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
name=""
|
||||
value={destinationParentId}
|
||||
placeholder=" "
|
||||
className={`${styles.formInput} ${styles.short}`}
|
||||
onChange={(e) => setDestinationParentId(e.target.value)}
|
||||
/>
|
||||
<span className={styles.formError}>{error}</span>
|
||||
</p>
|
||||
</dd>
|
||||
<dd className={styles.full}>
|
||||
<img src={shuffle} className={styles.transOwner} alt="" />
|
||||
</dd>
|
||||
<dt>
|
||||
{t(getTranslationID("changeOwnerPopup.label.lowerLayerId"))}
|
||||
</dt>
|
||||
<dd className={styles.lowerTrans}>
|
||||
<select
|
||||
name=""
|
||||
className={`${styles.formInput} ${styles.short}`}
|
||||
value={selectedChildId ?? bulkDisplayName}
|
||||
onChange={onBulkChange}
|
||||
>
|
||||
<option value={bulkValue}>{bulkDisplayName}</option>
|
||||
{childrenLicenseInfos.map((child) => (
|
||||
<option key={child.accountId} value={child.accountId}>
|
||||
{child.accountId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className={styles.txName}>{selectedChildName}</span>
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
{/* 処理中や子アカウントが1件も存在しない場合、Saveボタンを押せないようにする */}
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(getTranslationID("common.label.save"))}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading && childrenLicenseInfos.length > 0
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
onClick={onSaveClick}
|
||||
disabled={isLoading || childrenLicenseInfos.length <= 0}
|
||||
/>
|
||||
</dd>
|
||||
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeOwnerPopup;
|
||||
@ -45,9 +45,10 @@ export const LicenseOrderPopup: React.FC<LicenseOrderPopupProps> = (props) => {
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) return;
|
||||
setIsPushOrderButton(false);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
}, [isLoading, onClose]);
|
||||
|
||||
// 画面からのパラメータ
|
||||
const poNumber = useSelector(selectPoNumber);
|
||||
|
||||
@ -10,12 +10,16 @@ import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
getCompanyNameAsync,
|
||||
getLicenseSummaryAsync,
|
||||
selecLicenseSummaryInfo,
|
||||
selectLicenseSummaryInfo,
|
||||
selectCompanyName,
|
||||
selectIsLoading,
|
||||
updateRestrictionStatusAsync,
|
||||
} from "features/license/licenseSummary";
|
||||
import { selectSelectedRow } from "features/license/partnerLicense";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { DelegationBar } from "components/delegate";
|
||||
import { TIERS } from "components/auth/constants";
|
||||
import { isAdminUser, isApproveTier } from "features/auth/utils";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import key from "../../assets/images/key.svg";
|
||||
@ -40,6 +44,8 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
// 代行操作用のトークンを取得する
|
||||
const delegationAccessToken = useSelector(selectDelegationAccessToken);
|
||||
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
// popup制御関係
|
||||
const [islicenseOrderPopupOpen, setIslicenseOrderPopupOpen] = useState(false);
|
||||
const [isCardLicenseActivatePopupOpen, setIsCardLicenseActivatePopupOpen] =
|
||||
@ -62,9 +68,12 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
}, [setIsLicenseOrderHistoryOpen]);
|
||||
|
||||
// apiからの値取得関係
|
||||
const licenseSummaryInfo = useSelector(selecLicenseSummaryInfo);
|
||||
const licenseSummaryInfo = useSelector(selectLicenseSummaryInfo);
|
||||
const companyName = useSelector(selectCompanyName);
|
||||
|
||||
const isTier1 = isApproveTier([TIERS.TIER1]);
|
||||
const isAdmin = isAdminUser();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
||||
dispatch(getCompanyNameAsync({ selectedRow }));
|
||||
@ -78,6 +87,35 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
}
|
||||
}, [onReturn]);
|
||||
|
||||
const onStorageAvailableChange = useCallback(
|
||||
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(
|
||||
t(
|
||||
getTranslationID(
|
||||
"LicenseSummaryPage.message.storageUnavalableSwitchingConfirm"
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const restricted = e.target.checked;
|
||||
const accountId = selectedRow?.accountId;
|
||||
// 本関数が実行されるときはselectedRowが存在する前提のため、accountIdが存在しない場合の処理は不要
|
||||
if (!accountId) return;
|
||||
const { meta } = await dispatch(
|
||||
updateRestrictionStatusAsync({ accountId, restricted })
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(getLicenseSummaryAsync({ selectedRow }));
|
||||
}
|
||||
},
|
||||
[dispatch, selectedRow, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* isPopupOpenがfalseの場合はポップアップのhtmlを生成しないように対応。これによりポップアップは都度生成されて初期化の考慮が減る */}
|
||||
@ -272,6 +310,27 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
</dl>
|
||||
</div>
|
||||
<div>
|
||||
{isTier1 && isAdmin && (
|
||||
<p
|
||||
className={`${styles.checkAvail} ${styles.alignRight}`}
|
||||
>
|
||||
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.formCheck}
|
||||
checked={licenseSummaryInfo.isStorageAvailable}
|
||||
disabled={isLoading}
|
||||
onChange={onStorageAvailableChange}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"LicenseSummaryPage.label.storageUnavailableCheckbox"
|
||||
)
|
||||
)}
|
||||
</label>
|
||||
</p>
|
||||
)}
|
||||
<dl
|
||||
className={`${styles.listVertical} ${styles.marginBtm3}`}
|
||||
>
|
||||
@ -289,17 +348,31 @@ export const LicenseSummary: React.FC<LicenseSummaryProps> = (
|
||||
)
|
||||
)}
|
||||
</dt>
|
||||
{/* Storage Usedの値表示をハイフンに置き換え */}
|
||||
{/* <dd>{licenseSummaryInfo.storageSize}GB</dd> */}
|
||||
<dd>-</dd>
|
||||
<dd>
|
||||
{/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */}
|
||||
{(
|
||||
licenseSummaryInfo.storageSize /
|
||||
1000 /
|
||||
1000 /
|
||||
1000
|
||||
).toFixed(3)}
|
||||
GB
|
||||
</dd>
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID("LicenseSummaryPage.label.usedSize")
|
||||
)}
|
||||
</dt>
|
||||
{/* Storage Usedの値表示をハイフンに置き換え */}
|
||||
{/* <dd>{licenseSummaryInfo.usedSize}GB</dd> */}
|
||||
<dd>-</dd>
|
||||
<dd>
|
||||
{/** Byte単位で受け取った値をGB単位で表示するため1000^3で割っている(小数点以下第三位まで表示で第四位で四捨五入) */}
|
||||
{(
|
||||
licenseSummaryInfo.usedSize /
|
||||
1000 /
|
||||
1000 /
|
||||
1000
|
||||
).toFixed(3)}
|
||||
GB
|
||||
</dd>
|
||||
<dt className={styles.overLine}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
|
||||
@ -12,6 +12,7 @@ import { CardLicenseIssuePopup } from "./cardLicenseIssuePopup";
|
||||
import postAdd from "../../assets/images/post_add.svg";
|
||||
import history from "../../assets/images/history.svg";
|
||||
import returnLabel from "../../assets/images/undo.svg";
|
||||
import changeOwnerIcon from "../../assets/images/change_circle.svg";
|
||||
import { isApproveTier } from "../../features/auth/utils";
|
||||
import { TIERS } from "../../components/auth/constants";
|
||||
import {
|
||||
@ -37,6 +38,7 @@ import { LicenseOrderPopup } from "./licenseOrderPopup";
|
||||
import { LicenseOrderHistory } from "./licenseOrderHistory";
|
||||
import { LicenseSummary } from "./licenseSummary";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import ChangeOwnerPopup from "./changeOwnerPopup";
|
||||
|
||||
const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -49,6 +51,7 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
const [islicenseOrderHistoryOpen, setIslicenseOrderHistoryOpen] =
|
||||
useState(false);
|
||||
const [isViewDetailsOpen, setIsViewDetailsOpen] = useState(false);
|
||||
const [isChangeOwnerPopupOpen, setIsChangeOwnerPopupOpen] = useState(false);
|
||||
|
||||
// 階層表示用
|
||||
const tierNames: { [key: number]: string } = {
|
||||
@ -148,6 +151,11 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
[dispatch, setIslicenseOrderHistoryOpen]
|
||||
);
|
||||
|
||||
// changeOwnerボタン押下時
|
||||
const onClickChangeOwner = useCallback(() => {
|
||||
setIsChangeOwnerPopupOpen(true);
|
||||
}, [setIsChangeOwnerPopupOpen]);
|
||||
|
||||
// マウント時のみ実行
|
||||
useEffect(() => {
|
||||
dispatch(getMyAccountAsync());
|
||||
@ -245,6 +253,13 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isChangeOwnerPopupOpen && (
|
||||
<ChangeOwnerPopup
|
||||
onClose={() => {
|
||||
setIsChangeOwnerPopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!islicenseOrderHistoryOpen && !isViewDetailsOpen && (
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
@ -329,6 +344,26 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{isVisibleChangeOwner(ownPartnerLicenseInfo.tier) && (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={onClickChangeOwner}
|
||||
>
|
||||
<img
|
||||
src={changeOwnerIcon}
|
||||
alt=""
|
||||
className={styles.menuIcon}
|
||||
/>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerLicense.label.changeOwnerButton"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<ul className={styles.brCrumbLicense}>
|
||||
{hierarchicalElements.map((value) => (
|
||||
@ -545,4 +580,10 @@ const PartnerLicense: React.FC = (): JSX.Element => {
|
||||
);
|
||||
};
|
||||
|
||||
const isVisibleChangeOwner = (partnerTier: number) =>
|
||||
// 自身が第一階層または第二階層で、表示しているパートナーが第三階層または第四階層の場合のみ表示
|
||||
isApproveTier([TIERS.TIER1, TIERS.TIER2]) &&
|
||||
(partnerTier.toString() === TIERS.TIER3 ||
|
||||
partnerTier.toString() === TIERS.TIER4);
|
||||
|
||||
export default PartnerLicense;
|
||||
|
||||
@ -0,0 +1,178 @@
|
||||
import { AppDispatch } from "app/store";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getTranslationID } from "translation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
changeEditCompanyName,
|
||||
changeSelectedAdminId,
|
||||
cleanupPartnerAccount,
|
||||
getPartnerUsersAsync,
|
||||
editPartnerInfoAsync,
|
||||
selectEditPartnerCompanyName,
|
||||
selectEditPartnerCountry,
|
||||
selectEditPartnerId,
|
||||
selectEditPartnerUsers,
|
||||
selectIsLoading,
|
||||
selectSelectedAdminId,
|
||||
selectOffset,
|
||||
getPartnerInfoAsync,
|
||||
LIMIT_PARTNER_VIEW_NUM,
|
||||
} from "features/partner";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import { COUNTRY_LIST } from "../SignupPage/constants";
|
||||
|
||||
interface EditPartnerAccountPopup {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const EditPartnerAccountPopup: React.FC<EditPartnerAccountPopup> = (
|
||||
props
|
||||
) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
const offset = useSelector(selectOffset);
|
||||
|
||||
const partnerId = useSelector(selectEditPartnerId);
|
||||
const companyName = useSelector(selectEditPartnerCompanyName);
|
||||
const country = useSelector(selectEditPartnerCountry);
|
||||
|
||||
const users = useSelector(selectEditPartnerUsers);
|
||||
const adminUser = users.find((user) => user.isPrimaryAdmin);
|
||||
|
||||
const selectedAdminId = useSelector(selectSelectedAdminId);
|
||||
|
||||
// ポップアップを閉じる処理
|
||||
const closePopup = useCallback(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
dispatch(cleanupPartnerAccount());
|
||||
onClose();
|
||||
}, [isLoading, onClose, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
dispatch(getPartnerUsersAsync({ accountId: partnerId }));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen]);
|
||||
|
||||
const onEditPartner = useCallback(async () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
if (!window.confirm(t(getTranslationID("common.message.dialogConfirm")))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(editPartnerInfoAsync());
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(
|
||||
getPartnerInfoAsync({
|
||||
limit: LIMIT_PARTNER_VIEW_NUM,
|
||||
offset,
|
||||
})
|
||||
);
|
||||
closePopup();
|
||||
}
|
||||
}, [dispatch, closePopup, t, offset]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("partnerPage.label.editAccount"))}
|
||||
<button type="button" onClick={closePopup}>
|
||||
<img src={close} className={styles.modalTitleIcon} alt="close" />
|
||||
</button>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl className={`${styles.formList} ${styles.hasbg}`}>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("partnerPage.label.accountInformation"))}
|
||||
</dt>
|
||||
<dt>{t(getTranslationID("partnerPage.label.name"))}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
maxLength={255}
|
||||
value={companyName}
|
||||
className={styles.formInput}
|
||||
onChange={(e) => {
|
||||
dispatch(
|
||||
changeEditCompanyName({ companyName: e.target.value })
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("partnerPage.label.country"))}</dt>
|
||||
<dd className={styles.last}>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
value={COUNTRY_LIST.find((c) => c.value === country)?.label}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("partnerPage.label.primaryAdminInfo"))}
|
||||
</dt>
|
||||
<dt>{t(getTranslationID("partnerPage.label.adminName"))}</dt>
|
||||
<dd>
|
||||
<input
|
||||
type="text"
|
||||
size={40}
|
||||
value={adminUser?.name}
|
||||
className={styles.formInput}
|
||||
readOnly
|
||||
/>
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("partnerPage.label.email"))}</dt>
|
||||
<dd className={styles.last}>
|
||||
<select
|
||||
className={styles.formInput}
|
||||
onChange={(event) => {
|
||||
dispatch(
|
||||
changeSelectedAdminId({
|
||||
adminId: Number(event.target.value),
|
||||
})
|
||||
);
|
||||
}}
|
||||
value={selectedAdminId}
|
||||
>
|
||||
{users.map((user) => (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.email}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(getTranslationID("partnerPage.label.saveChanges"))}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading && companyName.length !== 0 ? styles.isActive : ""
|
||||
}`}
|
||||
onClick={onEditPartner}
|
||||
/>
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -16,23 +16,28 @@ import {
|
||||
selectTotalPage,
|
||||
getPartnerInfoAsync,
|
||||
selectPartnersInfo,
|
||||
deletePartnerAccountAsync,
|
||||
} from "features/partner/index";
|
||||
import {
|
||||
changeDelegateAccount,
|
||||
changeEditPartner,
|
||||
savePageInfo,
|
||||
} from "features/partner/partnerSlice";
|
||||
import { getTranslationID } from "translation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getDelegationTokenAsync } from "features/auth/operations";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Partner } from "api";
|
||||
import personAdd from "../../assets/images/person_add.svg";
|
||||
import { TIERS } from "../../components/auth/constants";
|
||||
import { AddPartnerAccountPopup } from "./addPartnerAccountPopup";
|
||||
import { EditPartnerAccountPopup } from "./editPartnerAccountPopup";
|
||||
import checkFill from "../../assets/images/check_fill.svg";
|
||||
|
||||
const PartnerPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const [isPopupOpen, setIsPopupOpen] = useState(false);
|
||||
const [isEditPopupOpen, setIsEditPopupOpen] = useState(false);
|
||||
const [t] = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const total = useSelector(selectTotal);
|
||||
@ -71,6 +76,19 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
const onOpen = useCallback(() => {
|
||||
setIsPopupOpen(true);
|
||||
}, [setIsPopupOpen]);
|
||||
const onOpenEditPopup = useCallback(
|
||||
(editPartner: Partner) => {
|
||||
dispatch(
|
||||
changeEditPartner({
|
||||
id: editPartner.accountId,
|
||||
companyName: editPartner.name,
|
||||
country: editPartner.country,
|
||||
})
|
||||
);
|
||||
setIsEditPopupOpen(true);
|
||||
},
|
||||
[setIsEditPopupOpen, dispatch]
|
||||
);
|
||||
|
||||
// パートナー取得APIを呼び出す
|
||||
useEffect(() => {
|
||||
@ -109,6 +127,31 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
[dispatch, navigate, t]
|
||||
);
|
||||
|
||||
// delete account押下時処理
|
||||
const onDeleteAccount = useCallback(
|
||||
async (accountId: number, companyName: string) => {
|
||||
// ダイアログ確認
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(
|
||||
`${t(
|
||||
getTranslationID("partnerPage.message.partnerDeleteConfirm")
|
||||
)} ${companyName}`
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(deletePartnerAccountAsync({ accountId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(
|
||||
getPartnerInfoAsync({ limit: LIMIT_PARTNER_VIEW_NUM, offset })
|
||||
);
|
||||
}
|
||||
},
|
||||
[dispatch, t, offset]
|
||||
);
|
||||
|
||||
// HTML
|
||||
return (
|
||||
<>
|
||||
@ -118,6 +161,12 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
setIsPopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
<EditPartnerAccountPopup
|
||||
isOpen={isEditPopupOpen}
|
||||
onClose={() => {
|
||||
setIsEditPopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.wrap}>
|
||||
<Header />
|
||||
<UpdateTokenTimer />
|
||||
@ -185,10 +234,30 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
<tr>
|
||||
<td className={styles.clm0}>
|
||||
<ul className={styles.menuInTable}>
|
||||
{/* パートナーアカウント削除はCCB後回し分なので非表示
|
||||
{isVisibleButton && (
|
||||
<li>
|
||||
<a>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
onOpenEditPopup(x);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerPage.label.editAccount"
|
||||
)
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
{isVisibleButton && (
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
onDeleteAccount(x.accountId, x.name);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"partnerPage.label.deleteAccount"
|
||||
@ -197,7 +266,6 @@ const PartnerPage: React.FC = (): JSX.Element => {
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
*/}
|
||||
{isVisibleDealerManagement && (
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { AppDispatch } from "app/store";
|
||||
import Header from "components/header";
|
||||
@ -13,6 +13,7 @@ import {
|
||||
selectTemplates,
|
||||
listTemplateAsync,
|
||||
selectIsLoading,
|
||||
deleteTemplateAsync,
|
||||
} from "features/workflow/template";
|
||||
import { selectDelegationAccessToken } from "features/auth/selectors";
|
||||
import { DelegationBar } from "components/delegate";
|
||||
@ -35,6 +36,23 @@ export const TemplateFilePage: React.FC = () => {
|
||||
dispatch(listTemplateAsync());
|
||||
}, [dispatch]);
|
||||
|
||||
const onDeleteTemplate = useCallback(
|
||||
async (templateFileId: number) => {
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(deleteTemplateAsync({ templateFileId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(listTemplateAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isShowAddPopup && (
|
||||
@ -101,16 +119,17 @@ export const TemplateFilePage: React.FC = () => {
|
||||
<td>{template.name}</td>
|
||||
<td>
|
||||
<ul className={`${styles.menuAction} ${styles.inTable}`}>
|
||||
{/* テンプレートファイル削除はCCB後回し分なので非表示
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
href=""
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={() => {
|
||||
onDeleteTemplate(template.id);
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.label.delete"))}
|
||||
</a>
|
||||
</li>
|
||||
*/}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
selectTypistGroups,
|
||||
selectIsLoading,
|
||||
listTypistGroupsAsync,
|
||||
deleteTypistGroupAsync,
|
||||
} from "features/workflow/typistGroup";
|
||||
import { AppDispatch } from "app/store";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@ -47,6 +48,25 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
|
||||
[setIsEditPopupOpen]
|
||||
);
|
||||
|
||||
const onDeleteTypistGroup = useCallback(
|
||||
async (typistGroupId: number) => {
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(
|
||||
deleteTypistGroupAsync({ typistGroupId })
|
||||
);
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(listTypistGroupsAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(listTypistGroupsAsync());
|
||||
}, [dispatch]);
|
||||
@ -142,6 +162,17 @@ const TypistGroupSettingPage: React.FC = (): JSX.Element => {
|
||||
{t(getTranslationID("common.label.edit"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={() => {
|
||||
onDeleteTypistGroup(group.id);
|
||||
}}
|
||||
>
|
||||
{t(getTranslationID("common.label.delete"))}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
258
dictation_client/src/pages/UserListPage/importPopup.tsx
Normal file
258
dictation_client/src/pages/UserListPage/importPopup.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
import { AppDispatch } from "app/store";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import styles from "styles/app.module.scss";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getTranslationID } from "translation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
selectIsLoading,
|
||||
importUsersAsync,
|
||||
changeImportFileName,
|
||||
changeImportCsv,
|
||||
selectImportFileName,
|
||||
selectImportValidationErrors,
|
||||
cleanupImportUsers,
|
||||
} from "features/user";
|
||||
import { parseCSV } from "common/parser";
|
||||
import close from "../../assets/images/close.svg";
|
||||
import download from "../../assets/images/download.svg";
|
||||
import upload from "../../assets/images/upload.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
|
||||
interface UserAddPopupProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ImportPopup: React.FC<UserAddPopupProps> = (props) => {
|
||||
const { isOpen, onClose } = props;
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
// AddUserの情報を取得
|
||||
|
||||
const closePopup = useCallback(() => {
|
||||
setIsPushImportButton(false);
|
||||
dispatch(cleanupImportUsers());
|
||||
onClose();
|
||||
}, [onClose, dispatch]);
|
||||
|
||||
const [isPushImportButton, setIsPushImportButton] = useState<boolean>(false);
|
||||
const isLoading = useSelector(selectIsLoading);
|
||||
|
||||
const importFileName = useSelector(selectImportFileName);
|
||||
const { invalidInput, duplicatedEmails, duplicatedAuthorIds, overMaxRow } =
|
||||
useSelector(selectImportValidationErrors);
|
||||
|
||||
const onDownloadCsv = useCallback(() => {
|
||||
// csvファイルダウンロード処理
|
||||
const filename = `import_users.csv`;
|
||||
|
||||
const importCsvHeader = [
|
||||
"name",
|
||||
"email",
|
||||
"role",
|
||||
"author_id",
|
||||
"auto_assign",
|
||||
"notification",
|
||||
"encryption",
|
||||
"encryption_password",
|
||||
"prompt",
|
||||
].toString();
|
||||
|
||||
const blob = new Blob([importCsvHeader], {
|
||||
type: "mime",
|
||||
});
|
||||
const blobURL = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = blobURL;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.parentNode?.removeChild(a);
|
||||
}, []);
|
||||
|
||||
// ファイルが選択されたときの処理
|
||||
const handleFileChange = useCallback(
|
||||
async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// 選択されたファイルを取得(複数選択されても先頭を取得)
|
||||
const file = event.target.files?.[0];
|
||||
|
||||
// ファイルが選択されていれば、storeに保存
|
||||
if (file) {
|
||||
const text = await file.text();
|
||||
const users = await parseCSV(text.trimEnd());
|
||||
|
||||
dispatch(changeImportCsv({ users }));
|
||||
dispatch(changeImportFileName({ fileName: file.name }));
|
||||
}
|
||||
// 同名のファイルを選択した場合、onChangeが発火しないため、valueをクリアする
|
||||
event.target.value = "";
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onImportUsers = useCallback(async () => {
|
||||
setIsPushImportButton(true);
|
||||
if (
|
||||
invalidInput.length > 0 ||
|
||||
duplicatedEmails.length > 0 ||
|
||||
duplicatedAuthorIds.length > 0 ||
|
||||
overMaxRow
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await dispatch(importUsersAsync());
|
||||
setIsPushImportButton(false);
|
||||
}, [
|
||||
dispatch,
|
||||
invalidInput,
|
||||
duplicatedEmails,
|
||||
duplicatedAuthorIds,
|
||||
overMaxRow,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={`${styles.modal} ${isOpen ? styles.isShow : ""}`}>
|
||||
<div className={styles.modalBox}>
|
||||
<p className={styles.modalTitle}>
|
||||
{t(getTranslationID("userListPage.label.bulkImport"))}
|
||||
<button type="button" onClick={closePopup}>
|
||||
<img src={close} className={styles.modalTitleIcon} alt="close" />
|
||||
</button>
|
||||
</p>
|
||||
<form className={styles.form}>
|
||||
<dl
|
||||
className={`${styles.formList} ${styles.userImport} ${styles.hasbg}`}
|
||||
>
|
||||
<dd className={styles.full}>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
style={{ marginInlineEnd: "350px", marginTop: "15px" }}
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={onDownloadCsv}
|
||||
>
|
||||
<img src={download} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.downloadCsv"))}
|
||||
</a>
|
||||
{t(getTranslationID("userListPage.text.downloadExplain"))}
|
||||
</dd>
|
||||
<dd className={styles.full}>
|
||||
<label
|
||||
style={{ marginInlineEnd: "350px", marginTop: "15px" }}
|
||||
htmlFor="import"
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="import"
|
||||
style={{ display: "none" }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
<img src={upload} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.importCsv"))}
|
||||
</label>
|
||||
</dd>
|
||||
<dt className={styles.formTitle}>
|
||||
{t(getTranslationID("userListPage.label.inputRules"))}
|
||||
</dt>
|
||||
<dt>{t(getTranslationID("userListPage.label.nameLabel"))}</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.nameRule"))}</dd>
|
||||
<dt>
|
||||
{t(getTranslationID("userListPage.label.emailAddressLabel"))}
|
||||
</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.emailAddressRule"))}</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.roleLabel"))}</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.roleRule"))}</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.authorIdLabel"))}</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.authorIdRule"))}</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.autoRenewLabel"))}</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.autoRenewRule"))}</dd>
|
||||
<dt>
|
||||
{t(getTranslationID("userListPage.label.notificationLabel"))}
|
||||
</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.notificationRule"))}</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.encryptionLabel"))}</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.encryptionRule"))}</dd>
|
||||
<dt>
|
||||
{t(
|
||||
getTranslationID("userListPage.label.encryptionPasswordLabel")
|
||||
)}
|
||||
</dt>
|
||||
<dd>
|
||||
{t(getTranslationID("userListPage.text.encryptionPasswordRule"))}
|
||||
</dd>
|
||||
<dt>{t(getTranslationID("userListPage.label.promptLabel"))}</dt>
|
||||
<dd>{t(getTranslationID("userListPage.text.promptRule"))}</dd>
|
||||
<dd className={styles.full}>
|
||||
{isPushImportButton && overMaxRow && (
|
||||
<span className={styles.formError}>
|
||||
{t(getTranslationID("userListPage.message.overMaxUserError"))}
|
||||
</span>
|
||||
)}
|
||||
{isPushImportButton && invalidInput.length > 0 && (
|
||||
<>
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID("userListPage.message.invalidInputError")
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.formError}>
|
||||
{invalidInput.map((row) => `L${row}`).join(", ")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isPushImportButton && duplicatedEmails.length > 0 && (
|
||||
<>
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.message.duplicateEmailError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.formError}>
|
||||
{duplicatedEmails.map((row) => `L${row}`).join(", ")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isPushImportButton && duplicatedAuthorIds.length > 0 && (
|
||||
<>
|
||||
<span className={styles.formError}>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.message.duplicateAuthorIdError"
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
<span className={styles.formError}>
|
||||
{duplicatedAuthorIds.map((row) => `L${row}`).join(", ")}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</dd>
|
||||
<dd className={`${styles.full} ${styles.alignCenter}`}>
|
||||
<input
|
||||
type="button"
|
||||
name="submit"
|
||||
value={t(getTranslationID("userListPage.label.addUsers"))}
|
||||
className={`${styles.formSubmit} ${styles.marginBtm1} ${
|
||||
!isLoading && importFileName !== undefined
|
||||
? styles.isActive
|
||||
: ""
|
||||
}`}
|
||||
onClick={onImportUsers}
|
||||
/>
|
||||
<img
|
||||
style={{ display: isLoading ? "inline" : "none" }}
|
||||
src={progress_activit}
|
||||
className={styles.icLoading}
|
||||
alt="Loading"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -10,6 +10,7 @@ import {
|
||||
selectUserViews,
|
||||
selectIsLoading,
|
||||
deallocateLicenseAsync,
|
||||
deleteUserAsync,
|
||||
} from "features/user";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getTranslationID } from "translation";
|
||||
@ -31,9 +32,11 @@ import personAdd from "../../assets/images/person_add.svg";
|
||||
import checkFill from "../../assets/images/check_fill.svg";
|
||||
import checkOutline from "../../assets/images/check_outline.svg";
|
||||
import progress_activit from "../../assets/images/progress_activit.svg";
|
||||
import upload from "../../assets/images/upload.svg";
|
||||
import { UserAddPopup } from "./popup";
|
||||
import { UserUpdatePopup } from "./updatePopup";
|
||||
import { AllocateLicensePopup } from "./allocateLicensePopup";
|
||||
import { ImportPopup } from "./importPopup";
|
||||
|
||||
const UserListPage: React.FC = (): JSX.Element => {
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
@ -45,6 +48,7 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
const [isUpdatePopupOpen, setIsUpdatePopupOpen] = useState(false);
|
||||
const [isAllocateLicensePopupOpen, setIsAllocateLicensePopupOpen] =
|
||||
useState(false);
|
||||
const [isImportPopupOpen, setIsImportPopupOpen] = useState(false);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setIsPopupOpen(true);
|
||||
@ -65,6 +69,9 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
},
|
||||
[setIsAllocateLicensePopupOpen, dispatch]
|
||||
);
|
||||
const onImportPopupOpen = useCallback(() => {
|
||||
setIsImportPopupOpen(true);
|
||||
}, [setIsImportPopupOpen]);
|
||||
|
||||
const onLicenseDeallocation = useCallback(
|
||||
async (userId: number) => {
|
||||
@ -84,6 +91,24 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
const onDeleteUser = useCallback(
|
||||
async (userId: number) => {
|
||||
// ダイアログ確認
|
||||
if (
|
||||
/* eslint-disable-next-line no-alert */
|
||||
!window.confirm(t(getTranslationID("common.message.dialogConfirm")))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { meta } = await dispatch(deleteUserAsync({ userId }));
|
||||
if (meta.requestStatus === "fulfilled") {
|
||||
dispatch(listUsersAsync());
|
||||
}
|
||||
},
|
||||
[dispatch, t]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// ユーザ一覧取得処理を呼び出す
|
||||
dispatch(listUsersAsync());
|
||||
@ -115,6 +140,12 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
setIsAllocateLicensePopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
<ImportPopup
|
||||
isOpen={isImportPopupOpen}
|
||||
onClose={() => {
|
||||
setIsImportPopupOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.wrap} ${
|
||||
delegationAccessToken ? styles.manage : ""
|
||||
@ -146,6 +177,16 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
{t(getTranslationID("userListPage.label.addUser"))}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
className={`${styles.menuLink} ${styles.isActive}`}
|
||||
onClick={onImportPopupOpen}
|
||||
>
|
||||
<img src={upload} alt="" className={styles.menuIcon} />
|
||||
{t(getTranslationID("userListPage.label.bulkImport"))}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.tableWrap}>
|
||||
<table className={`${styles.table} ${styles.user}`}>
|
||||
@ -255,9 +296,13 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{/* ユーザー削除 CCB後回し分なので今は非表示
|
||||
<li>
|
||||
<a href="">
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events,jsx-a11y/no-static-element-interactions */}
|
||||
<a
|
||||
onClick={() => {
|
||||
onDeleteUser(user.id);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
getTranslationID(
|
||||
"userListPage.label.deleteUser"
|
||||
@ -265,7 +310,6 @@ const UserListPage: React.FC = (): JSX.Element => {
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
*/}
|
||||
</ul>
|
||||
</td>
|
||||
<td> {user.name}</td>
|
||||
|
||||
@ -15,11 +15,8 @@ const UserVerifyPage: React.FC = (): JSX.Element => {
|
||||
const jwt = query.get("verify") ?? "";
|
||||
|
||||
useEffect(() => {
|
||||
if (!jwt) {
|
||||
navigate("/mail-confirm/failed");
|
||||
}
|
||||
dispatch(userVerifyAsync({ jwt }));
|
||||
}, [navigate, dispatch, jwt]);
|
||||
}, [dispatch, jwt]);
|
||||
|
||||
const verifyState = useSelector(VerifyStateSelector);
|
||||
|
||||
|
||||
@ -1630,6 +1630,43 @@ _:-ms-lang(x)::-ms-backdrop,
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
.formList.userImport .formTitle {
|
||||
padding: 1rem 4% 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.formList.userImport dt:not(.formTitle) {
|
||||
width: 30%;
|
||||
padding: 0 4% 0 4%;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.formList.userImport dt:not(.formTitle):nth-of-type(odd) {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.formList.userImport dt:not(.formTitle):nth-of-type(odd) + dd {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.formList.userImport dd {
|
||||
width: 58%;
|
||||
padding: 0.2rem 4% 0.2rem 0;
|
||||
margin-bottom: 0;
|
||||
white-space: pre-line;
|
||||
word-wrap: break-word;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.formList.userImport dd.full {
|
||||
width: 100%;
|
||||
padding: 0.2rem 4% 0.2rem 4%;
|
||||
}
|
||||
.formList.userImport dd.full .buttonText {
|
||||
padding: 0 0 0.8rem;
|
||||
}
|
||||
.formList.userImport dd .menuLink {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.6rem;
|
||||
padding: 0.5rem 1.5rem 0.5rem 1.3rem;
|
||||
}
|
||||
|
||||
.account .listVertical {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
@ -1857,6 +1894,18 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.license .checkAvail {
|
||||
height: 30px;
|
||||
padding: 0 0.3rem 0.3rem 0;
|
||||
margin-top: -30px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.license .checkAvail label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.license .checkAvail label .formCheck {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.license .listVertical dd img[src*="circle"] {
|
||||
filter: brightness(0) saturate(100%) invert(58%) sepia(41%) saturate(5814%)
|
||||
hue-rotate(143deg) brightness(96%) contrast(101%);
|
||||
@ -1950,6 +1999,61 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.formList dd.ownerChange {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.formList dd.ownerChange p.Owner,
|
||||
.formList dd.ownerChange p.newOwner {
|
||||
width: 150px;
|
||||
}
|
||||
.formList dd.ownerChange .arrowR {
|
||||
width: 8%;
|
||||
height: 20px;
|
||||
margin-top: 10px;
|
||||
margin-right: 2%;
|
||||
background: #e6e6e6;
|
||||
position: relative;
|
||||
}
|
||||
.formList dd.ownerChange .arrowR::after {
|
||||
content: "";
|
||||
border-top: 20px transparent solid;
|
||||
border-bottom: 20px transparent solid;
|
||||
border-left: 20px #e6e6e6 solid;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -15px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
.formList dd.ownerChange + .full {
|
||||
width: 66%;
|
||||
margin-left: 30%;
|
||||
margin-bottom: -10px;
|
||||
text-align: center;
|
||||
}
|
||||
.formList dd.ownerChange + .full .transOwner {
|
||||
width: 100px;
|
||||
}
|
||||
.formList dd.lowerTrans {
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.formList dd.lowerTrans select,
|
||||
.formList dd.lowerTrans span {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.formList dd .txName {
|
||||
display: block;
|
||||
width: 150px;
|
||||
padding: 0.2rem 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dictation .menuAction {
|
||||
margin-top: -1rem;
|
||||
height: 34px;
|
||||
@ -2272,6 +2376,9 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
.formList.property dt:not(.formTitle):nth-of-type(odd) + dd {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.formList.property dt:has(+ dd.hasInput) {
|
||||
padding-top: 0.4rem;
|
||||
}
|
||||
.formList.property dd {
|
||||
width: 58%;
|
||||
padding: 0.2rem 4% 0.2rem 0;
|
||||
@ -2283,6 +2390,16 @@ tr.isSelected .menuInTable li a.isDisable {
|
||||
.formList.property dd img {
|
||||
height: 1.1rem;
|
||||
}
|
||||
.formList.property dd .formInput.short {
|
||||
width: 250px;
|
||||
padding: 0.3rem 0.3rem 0.1rem;
|
||||
}
|
||||
.formList.property dd .formSubmit {
|
||||
min-width: auto;
|
||||
padding: 0.2rem 0.5rem;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
}
|
||||
.formList.property dd.full {
|
||||
width: 100%;
|
||||
padding: 0.2rem 4% 0.2rem 4%;
|
||||
|
||||
12
dictation_client/src/styles/app.module.scss.d.ts
vendored
12
dictation_client/src/styles/app.module.scss.d.ts
vendored
@ -108,11 +108,12 @@ declare const classNames: {
|
||||
readonly clm0: "clm0";
|
||||
readonly menuInTable: "menuInTable";
|
||||
readonly isSelected: "isSelected";
|
||||
readonly userImport: "userImport";
|
||||
readonly menuLink: "menuLink";
|
||||
readonly odd: "odd";
|
||||
readonly alignRight: "alignRight";
|
||||
readonly menuAction: "menuAction";
|
||||
readonly inTable: "inTable";
|
||||
readonly menuLink: "menuLink";
|
||||
readonly menuIcon: "menuIcon";
|
||||
readonly colorLink: "colorLink";
|
||||
readonly isDisable: "isDisable";
|
||||
@ -123,10 +124,18 @@ declare const classNames: {
|
||||
readonly txNormal: "txNormal";
|
||||
readonly manageIcon: "manageIcon";
|
||||
readonly manageIconClose: "manageIconClose";
|
||||
readonly checkAvail: "checkAvail";
|
||||
readonly history: "history";
|
||||
readonly cardHistory: "cardHistory";
|
||||
readonly partner: "partner";
|
||||
readonly isOpen: "isOpen";
|
||||
readonly ownerChange: "ownerChange";
|
||||
readonly Owner: "Owner";
|
||||
readonly newOwner: "newOwner";
|
||||
readonly arrowR: "arrowR";
|
||||
readonly transOwner: "transOwner";
|
||||
readonly lowerTrans: "lowerTrans";
|
||||
readonly txName: "txName";
|
||||
readonly alignLeft: "alignLeft";
|
||||
readonly displayOptions: "displayOptions";
|
||||
readonly tableFilter: "tableFilter";
|
||||
@ -192,6 +201,7 @@ declare const classNames: {
|
||||
readonly hideO10: "hideO10";
|
||||
readonly op10: "op10";
|
||||
readonly property: "property";
|
||||
readonly hasInput: "hasInput";
|
||||
readonly formChange: "formChange";
|
||||
readonly chooseMember: "chooseMember";
|
||||
readonly holdMember: "holdMember";
|
||||
|
||||
@ -24,6 +24,21 @@ 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
|
||||
|
||||
# COPY --from=golang:1.18-buster /usr/local/go/ /usr/local/go/
|
||||
ENV GO111MODULE=auto
|
||||
COPY library-scripts/go-debian.sh /tmp/library-scripts/
|
||||
RUN bash /tmp/library-scripts/go-debian.sh "1.18" "/usr/local/go" "${GOPATH}" "${USERNAME}" "false" \
|
||||
&& apt-get clean -y && rm -rf /tmp/library-scripts
|
||||
ENV PATH="/usr/local/go/bin:${PATH}"
|
||||
RUN mkdir -p /tmp/gotools \
|
||||
&& cd /tmp/gotools \
|
||||
&& export GOPATH=/tmp/gotools \
|
||||
&& export GOCACHE=/tmp/gotools/cache \
|
||||
# sql-migrate
|
||||
&& go install github.com/rubenv/sql-migrate/sql-migrate@v1.1.2 \
|
||||
&& mv /tmp/gotools/bin/* ${TARGET_GOPATH}/bin/ \
|
||||
&& rm -rf /tmp/gotools
|
||||
|
||||
# Update NPM
|
||||
RUN npm install -g npm
|
||||
|
||||
|
||||
@ -16,6 +16,15 @@ services:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
networks:
|
||||
- external
|
||||
test_mysql_db:
|
||||
image: mysql:8.0-bullseye
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root_password
|
||||
MYSQL_DATABASE: odms
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: password
|
||||
networks:
|
||||
- external
|
||||
networks:
|
||||
external:
|
||||
name: omds_network
|
||||
|
||||
31
dictation_function/.devcontainer/pipeline-docker-compose.yml
Normal file
31
dictation_function/.devcontainer/pipeline-docker-compose.yml
Normal file
@ -0,0 +1,31 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
dictation_function:
|
||||
build: .
|
||||
working_dir: /app/dictation_function
|
||||
volumes:
|
||||
- ../../:/app
|
||||
- node_modules:/app/dictation_function/node_modules
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
depends_on:
|
||||
- test_mysql_db
|
||||
networks:
|
||||
- dictation_function_network
|
||||
test_mysql_db:
|
||||
image: mysql:8.0-bullseye
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root_password
|
||||
MYSQL_DATABASE: odms
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: password
|
||||
networks:
|
||||
- dictation_function_network
|
||||
networks:
|
||||
dictation_function_network:
|
||||
name: test_dictation_function_network
|
||||
|
||||
# Data Volume として永続化する
|
||||
volumes:
|
||||
node_modules:
|
||||
@ -1,5 +1,6 @@
|
||||
DB_HOST=omds-mysql
|
||||
DB_PORT=3306
|
||||
DB_NAME=omds
|
||||
DB_NAME_CCB=omds_ccb
|
||||
DB_USERNAME=omdsdbuser
|
||||
DB_PASSWORD=omdsdbpass
|
||||
35
dictation_function/.env.test
Normal file
35
dictation_function/.env.test
Normal file
@ -0,0 +1,35 @@
|
||||
STAGE=local
|
||||
TENANT_NAME=tenantoname
|
||||
SIGNIN_FLOW_NAME=b2c_1_signin_dev
|
||||
ADB2C_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
ADB2C_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
ADB2C_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
ADB2C_ORIGIN=https://xxxxxxx.b2clogin.com/xxxxxxxx.onmicrosoft.com/b2c_1_signin_dev/
|
||||
KEY_VAULT_NAME=xxxxxxxxxxxxxxx
|
||||
SENDGRID_API_KEY=SG.XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
MAIL_FROM=noreply@se0223.com
|
||||
APP_DOMAIN=http://localhost:8081/
|
||||
REDIS_HOST=redis-cache
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=omdsredispass
|
||||
ADB2C_CACHE_TTL=86400
|
||||
STORAGE_ACCOUNT_NAME_US=saxxxxxxxxx
|
||||
STORAGE_ACCOUNT_NAME_AU=saxxxxxxxxx
|
||||
STORAGE_ACCOUNT_NAME_EU=saxxxxxxxxx
|
||||
STORAGE_ACCOUNT_NAME_IMPORT=saxxxxxxxxx
|
||||
STORAGE_ACCOUNT_NAME_ANALYSIS=saxxxxxxxxx
|
||||
STORAGE_ACCOUNT_KEY_US=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
|
||||
STORAGE_ACCOUNT_KEY_AU=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
|
||||
STORAGE_ACCOUNT_KEY_EU=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
|
||||
STORAGE_ACCOUNT_KEY_IMPORT=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
|
||||
STORAGE_ACCOUNT_KEY_ANALYSIS=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==
|
||||
STORAGE_ACCOUNT_ENDPOINT_US=https://saxxxxxxxxx.blob.core.windows.net/
|
||||
STORAGE_ACCOUNT_ENDPOINT_AU=https://saxxxxxxxxx.blob.core.windows.net/
|
||||
STORAGE_ACCOUNT_ENDPOINT_EU=https://saxxxxxxxxx.blob.core.windows.net/
|
||||
STORAGE_ACCOUNT_ENDPOINT_IMPORT=https://saxxxxxxxxx.blob.core.windows.net/
|
||||
STORAGE_ACCOUNT_ENDPOINT_ANALYSIS=https://saxxxxxxxxx.blob.core.windows.net/
|
||||
BASE_PATH=http://localhost:8081
|
||||
ACCESS_TOKEN_LIFETIME_WEB=7200000
|
||||
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA5IZZNgDew9eGmuFTezwdHYLSaJvUPPIKYoiOeVLD1paWNI51\n7Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3yCTR6wcWR3PfFJrl9vh5SOo79koZ\noJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbWFJXnDe0DVXYXpJLb4LAlF2XAyYX0\nSYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qSfiL9zWk9dvHoKzSnfSDzDFoFcEoV\nchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//mBNNaDHv83Yuw3mGShT73iJ0JQdk\nTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GOOQIDAQABAoIBADrwp7u097+dK/tw\nWD61n3DIGAqg/lmFt8X4IH8MKLSE/FKr16CS1bqwOEuIM3ZdUtDeXd9Xs7IsyEPE\n5ZwuXK7DSF0M4+Mj8Ip49Q0Aww9aUoLQU9HGfgN/r4599GTrt31clZXA/6Mlighq\ncOZgCcEfdItz8OMu5SQuOIW4CKkCuaWnPOP26UqZocaXNZfpZH0iFLATMMH/TT8x\nay9ToHTQYE17ijdQ/EOLSwoeDV1CU1CIE3P4YfLJjvpKptly5dTevriHEzBi70Jx\n/KEPUn9Jj2gZafrUxRVhmMbm1zkeYxL3gsqRuTzRjEeeILuZhSJyCkQZyUNARxsg\nQY4DZfECgYEA+YLKUtmYTx60FS6DJ4s31TAsXY8kwhq/lB9E3GBZKDd0DPayXEeK\n4UWRQDTT6MI6fedW69FOZJ5sFLp8HQpcssb4Weq9PCpDhNTx8MCbdH3Um5QR3vfW\naKq/1XM8MDUnx5XcNYd87Aw3azvJAvOPr69as8IPnj6sKaRR9uQjbYUCgYEA6nfV\n5j0qmn0EJXZJblk4mvvjLLoWSs17j9YlrZJlJxXMDFRYtgnelv73xMxOMvcGoxn5\nifs7dpaM2x5EmA6jVU5sYaB/beZGEPWqPYGyjIwXPvUGAAv8Gbnvpp+xlSco/Dum\nIq0w+43ry5/xWh6CjfrvKV0J2bDOiJwPEdu/8iUCgYEAnBBSvL+dpN9vhFAzeOh7\nY71eAqcmNsLEUcG9MJqTKbSFwhYMOewF0iHRWHeylEPokhfBJn8kqYrtz4lVWFTC\n5o/Nh3BsLNXCpbMMIapXkeWiti1HgE9ErPMgSkJpwz18RDpYIqM8X+jEQS6D7HSr\nyxfDg+w+GJza0rEVE3hfMIECgYBw+KZ2VfhmEWBjEHhXE+QjQMR3s320MwebCUqE\nNCpKx8TWF/naVC0MwfLtvqbbBY0MHyLN6d//xpA9r3rLbRojqzKrY2KiuDYAS+3n\nzssRzxoQOozWju+8EYu30/ADdqfXyIHG6X3VZs87AGiQzGyJLmP3oR1y5y7MQa09\nJI16hQKBgHK5uwJhGa281Oo5/FwQ3uYLymbNwSGrsOJXiEu2XwJEXwVi2ELOKh4/\n03pBk3Kva3fIwEK+vCzDNnxShIQqBE76/2I1K1whOfoUehhYvKHGaXl2j70Zz9Ks\nrkGW1cx7p+yDqATDrwHBHTHFh5bUTTn8dN40n0e0W/llurpbBkJM\n-----END RSA PRIVATE KEY-----\n"
|
||||
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5IZZNgDew9eGmuFTezwd\nHYLSaJvUPPIKYoiOeVLD1paWNI517Vkaoh0ngprcKOdv6T1N07V4igK7mOim2zY3\nyCTR6wcWR3PfFJrl9vh5SOo79koZoJb27YiM4jtxfx2dezzp0T2GoNR5rRolPUbW\nFJXnDe0DVXYXpJLb4LAlF2XAyYX0SYKUVUsJnzm5k4xbXtnwPwVbpm0EdswBE6qS\nfiL9zWk9dvHoKzSnfSDzDFoFcEoVchawzYXf/MM1YR4wo5XyzECc6Q5Ah4z522//\nmBNNaDHv83Yuw3mGShT73iJ0JQdkTturshv2Ecma38r6ftrIwNYXw4VVatJM8+GO\nOQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
2
dictation_function/.gitignore
vendored
2
dictation_function/.gitignore
vendored
@ -11,6 +11,8 @@ Publish
|
||||
*.Cache
|
||||
project.lock.json
|
||||
|
||||
.test/
|
||||
|
||||
/packages
|
||||
/TestResults
|
||||
|
||||
|
||||
2
dictation_function/codegen.sh
Normal file
2
dictation_function/codegen.sh
Normal file
@ -0,0 +1,2 @@
|
||||
npx openapi-generator-cli version-manager set 7.1.0
|
||||
npx openapi-generator-cli generate -g typescript-axios -i /app/dictation_function/src/api/odms/openapi.json -o /app/dictation_function/src/api/
|
||||
7
dictation_function/openapitools.json
Normal file
7
dictation_function/openapitools.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "7.1.0"
|
||||
}
|
||||
}
|
||||
1118
dictation_function/package-lock.json
generated
1118
dictation_function/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,11 +9,14 @@
|
||||
"clean": "rimraf dist",
|
||||
"prestart": "npm run clean && npm run build",
|
||||
"start": "func start",
|
||||
"test": "jest"
|
||||
"test": "tsc --noEmit && sql-migrate up -config=/app/dictation_server/db/dbconfig.yml -env=test && jest -w 1",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"codegen": "sh codegen.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/functions": "^4.0.0",
|
||||
"@azure/identity": "^3.1.3",
|
||||
"@azure/storage-blob": "^12.17.0",
|
||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||
"@sendgrid/mail": "^7.7.0",
|
||||
"dotenv": "^16.0.3",
|
||||
@ -22,11 +25,13 @@
|
||||
"typeorm": "^0.3.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openapitools/openapi-generator-cli": "^2.9.0",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/node": "18.x",
|
||||
"@types/redis": "^2.8.13",
|
||||
"@types/redis-mock": "^0.17.3",
|
||||
"azure-functions-core-tools": "^4.x",
|
||||
"base64url": "^3.0.1",
|
||||
"jest": "^28.0.3",
|
||||
"redis-mock": "^0.56.3",
|
||||
"rimraf": "^5.0.0",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export type AdB2cResponse = {
|
||||
'@odata.context': string;
|
||||
"@odata.context": string;
|
||||
value: AdB2cUser[];
|
||||
};
|
||||
export type AdB2cUser = {
|
||||
|
||||
4
dictation_function/src/api/.gitignore
vendored
Normal file
4
dictation_function/src/api/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
wwwroot/*.js
|
||||
node_modules
|
||||
typings
|
||||
dist
|
||||
1
dictation_function/src/api/.npmignore
Normal file
1
dictation_function/src/api/.npmignore
Normal file
@ -0,0 +1 @@
|
||||
# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm
|
||||
23
dictation_function/src/api/.openapi-generator-ignore
Normal file
23
dictation_function/src/api/.openapi-generator-ignore
Normal file
@ -0,0 +1,23 @@
|
||||
# OpenAPI Generator Ignore
|
||||
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
|
||||
|
||||
# Use this file to prevent files from being overwritten by the generator.
|
||||
# The patterns follow closely to .gitignore or .dockerignore.
|
||||
|
||||
# As an example, the C# client generator defines ApiClient.cs.
|
||||
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
|
||||
#ApiClient.cs
|
||||
|
||||
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
|
||||
#foo/*/qux
|
||||
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
|
||||
|
||||
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
|
||||
#foo/**/qux
|
||||
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
|
||||
|
||||
# You can also negate patterns with an exclamation (!).
|
||||
# For example, you can ignore all files in a docs folder with the file extension .md:
|
||||
#docs/*.md
|
||||
# Then explicitly reverse the ignore rule for a single file:
|
||||
#!docs/README.md
|
||||
8
dictation_function/src/api/.openapi-generator/FILES
Normal file
8
dictation_function/src/api/.openapi-generator/FILES
Normal file
@ -0,0 +1,8 @@
|
||||
.gitignore
|
||||
.npmignore
|
||||
api.ts
|
||||
base.ts
|
||||
common.ts
|
||||
configuration.ts
|
||||
git_push.sh
|
||||
index.ts
|
||||
1
dictation_function/src/api/.openapi-generator/VERSION
Normal file
1
dictation_function/src/api/.openapi-generator/VERSION
Normal file
@ -0,0 +1 @@
|
||||
7.1.0
|
||||
8686
dictation_function/src/api/api.ts
Normal file
8686
dictation_function/src/api/api.ts
Normal file
File diff suppressed because it is too large
Load Diff
86
dictation_function/src/api/base.ts
Normal file
86
dictation_function/src/api/base.ts
Normal file
@ -0,0 +1,86 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* ODMSOpenAPI
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from './configuration';
|
||||
// Some imports not used depending on template conditions
|
||||
// @ts-ignore
|
||||
import type { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import globalAxios from 'axios';
|
||||
|
||||
export const BASE_PATH = "http://localhost".replace(/\/+$/, "");
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const COLLECTION_FORMATS = {
|
||||
csv: ",",
|
||||
ssv: " ",
|
||||
tsv: "\t",
|
||||
pipes: "|",
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface RequestArgs
|
||||
*/
|
||||
export interface RequestArgs {
|
||||
url: string;
|
||||
options: AxiosRequestConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class BaseAPI
|
||||
*/
|
||||
export class BaseAPI {
|
||||
protected configuration: Configuration | undefined;
|
||||
|
||||
constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) {
|
||||
if (configuration) {
|
||||
this.configuration = configuration;
|
||||
this.basePath = configuration.basePath ?? basePath;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @class RequiredError
|
||||
* @extends {Error}
|
||||
*/
|
||||
export class RequiredError extends Error {
|
||||
constructor(public field: string, msg?: string) {
|
||||
super(msg);
|
||||
this.name = "RequiredError"
|
||||
}
|
||||
}
|
||||
|
||||
interface ServerMap {
|
||||
[key: string]: {
|
||||
url: string,
|
||||
description: string,
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const operationServerMap: ServerMap = {
|
||||
}
|
||||
150
dictation_function/src/api/common.ts
Normal file
150
dictation_function/src/api/common.ts
Normal file
@ -0,0 +1,150 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* ODMSOpenAPI
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
import type { Configuration } from "./configuration";
|
||||
import type { RequestArgs } from "./base";
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import { RequiredError } from "./base";
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const DUMMY_BASE_URL = 'https://example.com'
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws {RequiredError}
|
||||
* @export
|
||||
*/
|
||||
export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) {
|
||||
if (paramValue === null || paramValue === undefined) {
|
||||
throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) {
|
||||
if (configuration && configuration.apiKey) {
|
||||
const localVarApiKeyValue = typeof configuration.apiKey === 'function'
|
||||
? await configuration.apiKey(keyParamName)
|
||||
: await configuration.apiKey;
|
||||
object[keyParamName] = localVarApiKeyValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBasicAuthToObject = function (object: any, configuration?: Configuration) {
|
||||
if (configuration && (configuration.username || configuration.password)) {
|
||||
object["auth"] = { username: configuration.username, password: configuration.password };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const accessToken = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken()
|
||||
: await configuration.accessToken;
|
||||
object["Authorization"] = "Bearer " + accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) {
|
||||
if (configuration && configuration.accessToken) {
|
||||
const localVarAccessTokenValue = typeof configuration.accessToken === 'function'
|
||||
? await configuration.accessToken(name, scopes)
|
||||
: await configuration.accessToken;
|
||||
object["Authorization"] = "Bearer " + localVarAccessTokenValue;
|
||||
}
|
||||
}
|
||||
|
||||
function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void {
|
||||
if (parameter == null) return;
|
||||
if (typeof parameter === "object") {
|
||||
if (Array.isArray(parameter)) {
|
||||
(parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key));
|
||||
}
|
||||
else {
|
||||
Object.keys(parameter).forEach(currentKey =>
|
||||
setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`)
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (urlSearchParams.has(key)) {
|
||||
urlSearchParams.append(key, parameter);
|
||||
}
|
||||
else {
|
||||
urlSearchParams.set(key, parameter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const setSearchParams = function (url: URL, ...objects: any[]) {
|
||||
const searchParams = new URLSearchParams(url.search);
|
||||
setFlattenedQueryParams(searchParams, objects);
|
||||
url.search = searchParams.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) {
|
||||
const nonString = typeof value !== 'string';
|
||||
const needsSerialization = nonString && configuration && configuration.isJsonMime
|
||||
? configuration.isJsonMime(requestOptions.headers['Content-Type'])
|
||||
: nonString;
|
||||
return needsSerialization
|
||||
? JSON.stringify(value !== undefined ? value : {})
|
||||
: (value || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const toPathString = function (url: URL) {
|
||||
return url.pathname + url.search + url.hash
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
*/
|
||||
export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) {
|
||||
return <T = unknown, R = AxiosResponse<T>>(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
|
||||
const axiosRequestArgs = {...axiosArgs.options, url: (configuration?.basePath || axios.defaults.baseURL || basePath) + axiosArgs.url};
|
||||
return axios.request<T, R>(axiosRequestArgs);
|
||||
};
|
||||
}
|
||||
110
dictation_function/src/api/configuration.ts
Normal file
110
dictation_function/src/api/configuration.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* ODMSOpenAPI
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
export interface ConfigurationParameters {
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
username?: string;
|
||||
password?: string;
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
basePath?: string;
|
||||
serverIndex?: number;
|
||||
baseOptions?: any;
|
||||
formDataCtor?: new () => any;
|
||||
}
|
||||
|
||||
export class Configuration {
|
||||
/**
|
||||
* parameter for apiKey security
|
||||
* @param name security name
|
||||
* @memberof Configuration
|
||||
*/
|
||||
apiKey?: string | Promise<string> | ((name: string) => string) | ((name: string) => Promise<string>);
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
username?: string;
|
||||
/**
|
||||
* parameter for basic security
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
password?: string;
|
||||
/**
|
||||
* parameter for oauth2 security
|
||||
* @param name security name
|
||||
* @param scopes oauth2 scope
|
||||
* @memberof Configuration
|
||||
*/
|
||||
accessToken?: string | Promise<string> | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise<string>);
|
||||
/**
|
||||
* override base path
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
basePath?: string;
|
||||
/**
|
||||
* override server index
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
serverIndex?: number;
|
||||
/**
|
||||
* base options for axios calls
|
||||
*
|
||||
* @type {any}
|
||||
* @memberof Configuration
|
||||
*/
|
||||
baseOptions?: any;
|
||||
/**
|
||||
* The FormData constructor that will be used to create multipart form data
|
||||
* requests. You can inject this here so that execution environments that
|
||||
* do not support the FormData class can still run the generated client.
|
||||
*
|
||||
* @type {new () => FormData}
|
||||
*/
|
||||
formDataCtor?: new () => any;
|
||||
|
||||
constructor(param: ConfigurationParameters = {}) {
|
||||
this.apiKey = param.apiKey;
|
||||
this.username = param.username;
|
||||
this.password = param.password;
|
||||
this.accessToken = param.accessToken;
|
||||
this.basePath = param.basePath;
|
||||
this.serverIndex = param.serverIndex;
|
||||
this.baseOptions = param.baseOptions;
|
||||
this.formDataCtor = param.formDataCtor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given MIME is a JSON MIME.
|
||||
* JSON MIME examples:
|
||||
* application/json
|
||||
* application/json; charset=UTF8
|
||||
* APPLICATION/JSON
|
||||
* application/vnd.company+json
|
||||
* @param mime - MIME (Multipurpose Internet Mail Extensions)
|
||||
* @return True if the given MIME is JSON, false otherwise.
|
||||
*/
|
||||
public isJsonMime(mime: string): boolean {
|
||||
const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i');
|
||||
return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json');
|
||||
}
|
||||
}
|
||||
57
dictation_function/src/api/git_push.sh
Normal file
57
dictation_function/src/api/git_push.sh
Normal file
@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
|
||||
#
|
||||
# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com"
|
||||
|
||||
git_user_id=$1
|
||||
git_repo_id=$2
|
||||
release_note=$3
|
||||
git_host=$4
|
||||
|
||||
if [ "$git_host" = "" ]; then
|
||||
git_host="github.com"
|
||||
echo "[INFO] No command line input provided. Set \$git_host to $git_host"
|
||||
fi
|
||||
|
||||
if [ "$git_user_id" = "" ]; then
|
||||
git_user_id="GIT_USER_ID"
|
||||
echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id"
|
||||
fi
|
||||
|
||||
if [ "$git_repo_id" = "" ]; then
|
||||
git_repo_id="GIT_REPO_ID"
|
||||
echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id"
|
||||
fi
|
||||
|
||||
if [ "$release_note" = "" ]; then
|
||||
release_note="Minor update"
|
||||
echo "[INFO] No command line input provided. Set \$release_note to $release_note"
|
||||
fi
|
||||
|
||||
# Initialize the local directory as a Git repository
|
||||
git init
|
||||
|
||||
# Adds the files in the local repository and stages them for commit.
|
||||
git add .
|
||||
|
||||
# Commits the tracked changes and prepares them to be pushed to a remote repository.
|
||||
git commit -m "$release_note"
|
||||
|
||||
# Sets the new remote
|
||||
git_remote=$(git remote)
|
||||
if [ "$git_remote" = "" ]; then # git remote not defined
|
||||
|
||||
if [ "$GIT_TOKEN" = "" ]; then
|
||||
echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment."
|
||||
git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git
|
||||
else
|
||||
git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
git pull origin master
|
||||
|
||||
# Pushes (Forces) the changes in the local repository up to the remote repository
|
||||
echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git"
|
||||
git push origin master 2>&1 | grep -v 'To https'
|
||||
18
dictation_function/src/api/index.ts
Normal file
18
dictation_function/src/api/index.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* ODMSOpenAPI
|
||||
* No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
|
||||
*
|
||||
* The version of the OpenAPI document: 1.0.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
* https://openapi-generator.tech
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
|
||||
export * from "./api";
|
||||
export * from "./configuration";
|
||||
|
||||
5357
dictation_function/src/api/odms/openapi.json
Normal file
5357
dictation_function/src/api/odms/openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
146
dictation_function/src/blobstorage/audioBlobStorage.service.ts
Normal file
146
dictation_function/src/blobstorage/audioBlobStorage.service.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import {
|
||||
BlobServiceClient,
|
||||
ContainerClient,
|
||||
StorageSharedKeyCredential,
|
||||
} from "@azure/storage-blob";
|
||||
import {
|
||||
BLOB_STORAGE_REGION_AU,
|
||||
BLOB_STORAGE_REGION_EU,
|
||||
BLOB_STORAGE_REGION_US,
|
||||
} from "../constants";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
|
||||
export class AudioBlobStorageService {
|
||||
private readonly sharedKeyCredentialUS: StorageSharedKeyCredential;
|
||||
private readonly sharedKeyCredentialEU: StorageSharedKeyCredential;
|
||||
private readonly sharedKeyCredentialAU: StorageSharedKeyCredential;
|
||||
|
||||
private readonly blobServiceClientUS: BlobServiceClient;
|
||||
private readonly blobServiceClientEU: BlobServiceClient;
|
||||
private readonly blobServiceClientAU: BlobServiceClient;
|
||||
|
||||
constructor() {
|
||||
if (
|
||||
!process.env.STORAGE_ACCOUNT_ENDPOINT_US ||
|
||||
!process.env.STORAGE_ACCOUNT_ENDPOINT_AU ||
|
||||
!process.env.STORAGE_ACCOUNT_ENDPOINT_EU ||
|
||||
!process.env.STORAGE_ACCOUNT_NAME_US ||
|
||||
!process.env.STORAGE_ACCOUNT_KEY_US ||
|
||||
!process.env.STORAGE_ACCOUNT_NAME_AU ||
|
||||
!process.env.STORAGE_ACCOUNT_KEY_AU ||
|
||||
!process.env.STORAGE_ACCOUNT_NAME_EU ||
|
||||
!process.env.STORAGE_ACCOUNT_KEY_EU
|
||||
) {
|
||||
throw new Error("Storage account information is missing");
|
||||
}
|
||||
|
||||
// リージョンごとのSharedKeyCredentialを生成
|
||||
this.sharedKeyCredentialUS = new StorageSharedKeyCredential(
|
||||
process.env.STORAGE_ACCOUNT_NAME_US,
|
||||
process.env.STORAGE_ACCOUNT_KEY_US
|
||||
);
|
||||
this.sharedKeyCredentialAU = new StorageSharedKeyCredential(
|
||||
process.env.STORAGE_ACCOUNT_NAME_AU,
|
||||
process.env.STORAGE_ACCOUNT_KEY_AU
|
||||
);
|
||||
this.sharedKeyCredentialEU = new StorageSharedKeyCredential(
|
||||
process.env.STORAGE_ACCOUNT_NAME_EU,
|
||||
process.env.STORAGE_ACCOUNT_KEY_EU
|
||||
);
|
||||
|
||||
// Audioファイルの保存先のリージョンごとにBlobServiceClientを生成
|
||||
this.blobServiceClientUS = new BlobServiceClient(
|
||||
process.env.STORAGE_ACCOUNT_ENDPOINT_US,
|
||||
this.sharedKeyCredentialUS
|
||||
);
|
||||
this.blobServiceClientAU = new BlobServiceClient(
|
||||
process.env.STORAGE_ACCOUNT_ENDPOINT_AU,
|
||||
this.sharedKeyCredentialAU
|
||||
);
|
||||
this.blobServiceClientEU = new BlobServiceClient(
|
||||
process.env.STORAGE_ACCOUNT_ENDPOINT_EU,
|
||||
this.sharedKeyCredentialEU
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定されたファイルを削除します。
|
||||
* @param context
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @param fileName
|
||||
* @returns file
|
||||
*/
|
||||
async deleteFile(
|
||||
context: InvocationContext,
|
||||
accountId: number,
|
||||
country: string,
|
||||
fileName: string
|
||||
): Promise<void> {
|
||||
context.log(
|
||||
`[IN] ${this.deleteFile.name} | params: { ` +
|
||||
`accountId: ${accountId} ` +
|
||||
`country: ${country} ` +
|
||||
`fileName: ${fileName} };`
|
||||
);
|
||||
|
||||
try {
|
||||
// 国に応じたリージョンでコンテナ名を指定してClientを取得
|
||||
const containerClient = this.getContainerClient(
|
||||
context,
|
||||
accountId,
|
||||
country
|
||||
);
|
||||
// コンテナ内のBlobパス名を指定してClientを取得
|
||||
const blobClient = containerClient.getBlobClient(fileName);
|
||||
|
||||
const { succeeded, errorCode, date } = await blobClient.deleteIfExists();
|
||||
context.log(
|
||||
`succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`
|
||||
);
|
||||
|
||||
// 失敗時、Blobが存在しない場合以外はエラーとして例外をスローする
|
||||
// Blob不在の場合のエラーコードは「BlobNotFound」以下を参照
|
||||
// https://learn.microsoft.com/ja-jp/rest/api/storageservices/blob-service-error-codes
|
||||
if (!succeeded && errorCode !== "BlobNotFound") {
|
||||
throw new Error(
|
||||
`delete blob failed. succeeded: ${succeeded}, errorCode: ${errorCode}, date: ${date}`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
context.error(`error=${e}`);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.deleteFile.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定してアカウントIDと国に応じたリージョンのコンテナクライアントを取得します。
|
||||
* @param context
|
||||
* @param accountId
|
||||
* @param country
|
||||
* @returns
|
||||
*/
|
||||
private getContainerClient(
|
||||
context: InvocationContext,
|
||||
accountId: number,
|
||||
country: string
|
||||
): ContainerClient {
|
||||
context.log(
|
||||
`[IN] ${this.getContainerClient.name} | params: { ` +
|
||||
`accountId: ${accountId}; country: ${country} };`
|
||||
);
|
||||
|
||||
const containerName = `account-${accountId}`;
|
||||
if (BLOB_STORAGE_REGION_US.includes(country)) {
|
||||
return this.blobServiceClientUS.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_AU.includes(country)) {
|
||||
return this.blobServiceClientAU.getContainerClient(containerName);
|
||||
} else if (BLOB_STORAGE_REGION_EU.includes(country)) {
|
||||
return this.blobServiceClientEU.getContainerClient(containerName);
|
||||
} else {
|
||||
throw new Error("invalid country");
|
||||
}
|
||||
}
|
||||
}
|
||||
269
dictation_function/src/blobstorage/blobstorage.service.ts
Normal file
269
dictation_function/src/blobstorage/blobstorage.service.ts
Normal file
@ -0,0 +1,269 @@
|
||||
import {
|
||||
BlobServiceClient,
|
||||
StorageSharedKeyCredential,
|
||||
} from "@azure/storage-blob";
|
||||
import {
|
||||
IMPORT_USERS_CONTAINER_NAME,
|
||||
LICENSE_COUNT_ANALYSIS_CONTAINER_NAME,
|
||||
} from "../constants";
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
|
||||
export class BlobstorageService {
|
||||
private readonly blobServiceClient: BlobServiceClient;
|
||||
private readonly sharedKeyCredential: StorageSharedKeyCredential;
|
||||
|
||||
constructor(useAnalysisBlob: boolean = false) {
|
||||
if (!useAnalysisBlob) {
|
||||
if (
|
||||
!process.env.STORAGE_ACCOUNT_NAME_IMPORT ||
|
||||
!process.env.STORAGE_ACCOUNT_KEY_IMPORT ||
|
||||
!process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT
|
||||
) {
|
||||
throw new Error("Storage account information is missing");
|
||||
}
|
||||
|
||||
this.sharedKeyCredential = new StorageSharedKeyCredential(
|
||||
process.env.STORAGE_ACCOUNT_NAME_IMPORT,
|
||||
process.env.STORAGE_ACCOUNT_KEY_IMPORT
|
||||
);
|
||||
this.blobServiceClient = new BlobServiceClient(
|
||||
process.env.STORAGE_ACCOUNT_ENDPOINT_IMPORT,
|
||||
this.sharedKeyCredential
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
!process.env.STORAGE_ACCOUNT_NAME_ANALYSIS ||
|
||||
!process.env.STORAGE_ACCOUNT_KEY_ANALYSIS ||
|
||||
!process.env.STORAGE_ACCOUNT_ENDPOINT_ANALYSIS
|
||||
) {
|
||||
throw new Error("Storage account information for analysis is missing");
|
||||
}
|
||||
|
||||
this.sharedKeyCredential = new StorageSharedKeyCredential(
|
||||
process.env.STORAGE_ACCOUNT_NAME_ANALYSIS,
|
||||
process.env.STORAGE_ACCOUNT_KEY_ANALYSIS
|
||||
);
|
||||
this.blobServiceClient = new BlobServiceClient(
|
||||
process.env.STORAGE_ACCOUNT_ENDPOINT_ANALYSIS,
|
||||
this.sharedKeyCredential
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Lists blobs
|
||||
* @returns blobs
|
||||
*/
|
||||
async listBlobs(context: InvocationContext): Promise<string[]> {
|
||||
context.log(`[IN] ${this.listBlobs.name}`);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
|
||||
const blobNames: string[] = [];
|
||||
// stage.json以外のファイルを取得
|
||||
for await (const blob of containerClient.listBlobsFlat({
|
||||
prefix: "U_",
|
||||
})) {
|
||||
blobNames.push(blob.name);
|
||||
}
|
||||
|
||||
return blobNames;
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.listBlobs.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a blob
|
||||
* @param context
|
||||
* @param filename
|
||||
* @returns file buffer
|
||||
*/
|
||||
public async downloadFileData(
|
||||
context: InvocationContext,
|
||||
filename: string
|
||||
): Promise<string | undefined> {
|
||||
context.log(
|
||||
`[IN] ${this.downloadFileData.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
const blobClient = containerClient.getBlobClient(filename);
|
||||
try {
|
||||
const downloadBlockBlobResponse = await blobClient.downloadToBuffer();
|
||||
return downloadBlockBlobResponse.toString();
|
||||
} catch (error) {
|
||||
// ファイルが存在しない場合はundefinedを返す
|
||||
if (error?.statusCode === 404) {
|
||||
return undefined;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.downloadFileData.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates file
|
||||
* @param context
|
||||
* @param filename
|
||||
* @param data
|
||||
* @returns file
|
||||
*/
|
||||
public async updateFile(
|
||||
context: InvocationContext,
|
||||
filename: string,
|
||||
data: string
|
||||
): Promise<boolean> {
|
||||
context.log(
|
||||
`[IN] ${this.updateFile.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
|
||||
const { response } = await containerClient.uploadBlockBlob(
|
||||
filename,
|
||||
data,
|
||||
data.length
|
||||
);
|
||||
if (response.errorCode) {
|
||||
context.log(`update failed. response errorCode: ${response.errorCode}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.updateFile.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes file
|
||||
* @param context
|
||||
* @param filename
|
||||
* @returns file
|
||||
*/
|
||||
public async deleteFile(
|
||||
context: InvocationContext,
|
||||
filename: string
|
||||
): Promise<void> {
|
||||
context.log(
|
||||
`[IN] ${this.deleteFile.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
const blobClient = containerClient.getBlobClient(filename);
|
||||
await blobClient.deleteIfExists();
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.deleteFile.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether file exists is
|
||||
* @param context
|
||||
* @param filename
|
||||
* @returns file exists
|
||||
*/
|
||||
public async isFileExists(
|
||||
context: InvocationContext,
|
||||
filename: string
|
||||
): Promise<boolean> {
|
||||
context.log(
|
||||
`[IN] ${this.isFileExists.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
IMPORT_USERS_CONTAINER_NAME
|
||||
);
|
||||
const blobClient = containerClient.getBlobClient(filename);
|
||||
return await blobClient.exists();
|
||||
} catch (error) {
|
||||
context.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.isFileExists.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* uplocad file analysis licenses csv
|
||||
* @param context
|
||||
* @param filename
|
||||
* @param data
|
||||
* @returns boolean
|
||||
*/
|
||||
public async uploadFileAnalysisLicensesCSV(
|
||||
context: InvocationContext,
|
||||
filename: string,
|
||||
data: string
|
||||
): Promise<boolean> {
|
||||
context.log(
|
||||
`[IN] ${this.uploadFileAnalysisLicensesCSV.name} | params: { filename: ${filename} }`
|
||||
);
|
||||
try {
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
LICENSE_COUNT_ANALYSIS_CONTAINER_NAME
|
||||
);
|
||||
const { response } = await containerClient.uploadBlockBlob(
|
||||
filename,
|
||||
data,
|
||||
data.length
|
||||
);
|
||||
if (response.errorCode) {
|
||||
context.log(`update failed. response errorCode: ${response.errorCode}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.uploadFileAnalysisLicensesCSV.name}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Create container analysis
|
||||
* @param context
|
||||
* @returns container
|
||||
*/
|
||||
async createContainerAnalysis(context: InvocationContext): Promise<void> {
|
||||
context.log(`[IN] ${this.createContainerAnalysis.name}`);
|
||||
|
||||
try {
|
||||
// コンテナ作成
|
||||
const containerClient = this.blobServiceClient.getContainerClient(
|
||||
LICENSE_COUNT_ANALYSIS_CONTAINER_NAME
|
||||
);
|
||||
// コンテナが存在しない場合のみ作成
|
||||
if (await containerClient.exists()) {
|
||||
return;
|
||||
}
|
||||
await containerClient.create();
|
||||
} catch (e) {
|
||||
context.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
context.log(`[OUT] ${this.createContainerAnalysis.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
165
dictation_function/src/blobstorage/types/guards.ts
Normal file
165
dictation_function/src/blobstorage/types/guards.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { InvocationContext } from "@azure/functions";
|
||||
import { IMPORT_USERS_STAGES } from "../../constants";
|
||||
import { ErrorRow, ImportData, ImportJson, StageJson } from "./types";
|
||||
|
||||
const isErrorRow = (obj: any): obj is ErrorRow => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const errorRow = obj as ErrorRow;
|
||||
if (errorRow.name === undefined || typeof errorRow.name !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (errorRow.row === undefined || typeof errorRow.row !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (errorRow.error === undefined || typeof errorRow.error !== "string") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isStageJson = (obj: any): obj is StageJson => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const stageJson = obj as StageJson;
|
||||
if (
|
||||
stageJson.filename !== undefined &&
|
||||
typeof stageJson.filename !== "string"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (stageJson.update === undefined || typeof stageJson.update !== "number") {
|
||||
return false;
|
||||
}
|
||||
if (stageJson.row !== undefined && typeof stageJson.row !== "number") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
stageJson.errors !== undefined &&
|
||||
(!Array.isArray(stageJson.errors) ||
|
||||
!stageJson.errors.every((x) => isErrorRow(x)))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
stageJson.state === undefined ||
|
||||
!Object.values(IMPORT_USERS_STAGES).includes(stageJson.state)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const isImportData = (
|
||||
context: InvocationContext,
|
||||
obj: any
|
||||
): obj is ImportData => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const importData = obj as ImportData;
|
||||
if (importData.name === undefined || typeof importData.name !== "string") {
|
||||
context.log("name is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (importData.email === undefined || typeof importData.email !== "string") {
|
||||
context.log("email is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (importData.role === undefined || typeof importData.role !== "number") {
|
||||
context.log("role is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.author_id !== undefined &&
|
||||
typeof importData.author_id !== "string"
|
||||
) {
|
||||
context.log("author_id is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.auto_renew === undefined ||
|
||||
typeof importData.auto_renew !== "number"
|
||||
) {
|
||||
context.log("auto_renew is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.notification === undefined ||
|
||||
typeof importData.notification !== "number"
|
||||
) {
|
||||
context.log("notification is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.encryption !== undefined &&
|
||||
typeof importData.encryption !== "number"
|
||||
) {
|
||||
context.log("encryption is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.encryption_password !== undefined &&
|
||||
typeof importData.encryption_password !== "string"
|
||||
) {
|
||||
context.log("encryption_password is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importData.prompt !== undefined &&
|
||||
typeof importData.prompt !== "number"
|
||||
) {
|
||||
context.log("prompt is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isImportJson = (
|
||||
context: InvocationContext,
|
||||
obj: any
|
||||
): obj is ImportJson => {
|
||||
if (typeof obj !== "object") return false;
|
||||
const importJson = obj as ImportJson;
|
||||
if (
|
||||
importJson.account_id === undefined ||
|
||||
typeof importJson.account_id !== "number"
|
||||
) {
|
||||
context.log("account_id is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.user_id === undefined ||
|
||||
typeof importJson.user_id !== "number"
|
||||
) {
|
||||
context.log("user_id is missing or not a number");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.user_role === undefined ||
|
||||
typeof importJson.user_role !== "string"
|
||||
) {
|
||||
context.log("user_role is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.external_id === undefined ||
|
||||
typeof importJson.external_id !== "string"
|
||||
) {
|
||||
context.log("external_id is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.file_name === undefined ||
|
||||
typeof importJson.file_name !== "string"
|
||||
) {
|
||||
context.log("file_name is missing or not a string");
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
importJson.data === undefined ||
|
||||
!Array.isArray(importJson.data) ||
|
||||
!importJson.data.every((x) => isImportData(context, x))
|
||||
) {
|
||||
context.log("data is missing or not an array of ImportData");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
54
dictation_function/src/blobstorage/types/types.ts
Normal file
54
dictation_function/src/blobstorage/types/types.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { IMPORT_USERS_STAGES, USER_ROLES } from "../../constants";
|
||||
|
||||
export type StageType =
|
||||
(typeof IMPORT_USERS_STAGES)[keyof typeof IMPORT_USERS_STAGES];
|
||||
|
||||
export type StageJson = {
|
||||
filename?: string | undefined;
|
||||
update: number;
|
||||
row?: number | undefined;
|
||||
errors?: ErrorRow[] | undefined;
|
||||
state: StageType;
|
||||
};
|
||||
|
||||
export type ErrorRow = {
|
||||
name: string;
|
||||
row: number;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export type ImportJson = {
|
||||
account_id: number;
|
||||
user_id: number;
|
||||
user_role: RoleType;
|
||||
external_id: string;
|
||||
file_name: string;
|
||||
date: number;
|
||||
data: ImportData[];
|
||||
};
|
||||
|
||||
export type ImportData = {
|
||||
name: string;
|
||||
email: string;
|
||||
role: number;
|
||||
author_id?: string | undefined;
|
||||
auto_renew: number;
|
||||
notification: number;
|
||||
encryption?: number | undefined;
|
||||
encryption_password?: string | undefined;
|
||||
prompt?: number | undefined;
|
||||
};
|
||||
|
||||
export type RoleType = (typeof USER_ROLES)[keyof typeof USER_ROLES];
|
||||
|
||||
export type User = {
|
||||
name: string;
|
||||
role: RoleType;
|
||||
email: string;
|
||||
autoRenew: boolean;
|
||||
notification: boolean;
|
||||
authorId?: string | undefined;
|
||||
encryption?: boolean | undefined;
|
||||
encryptionPassword?: string | undefined;
|
||||
prompt?: boolean | undefined;
|
||||
};
|
||||
79
dictation_function/src/common/errors/code.ts
Normal file
79
dictation_function/src/common/errors/code.ts
Normal file
@ -0,0 +1,79 @@
|
||||
/*
|
||||
エラーコード作成方針
|
||||
E+6桁(数字)で構成する。
|
||||
- 1~2桁目の値は種類(業務エラー、システムエラー...)
|
||||
- 3~4桁目の値は原因箇所(トークン、DB、...)
|
||||
- 5~6桁目の値は任意の重複しない値
|
||||
ex)
|
||||
E00XXXX : システムエラー(通信エラーやDB接続失敗など)
|
||||
E01XXXX : 業務エラー
|
||||
EXX00XX : 内部エラー(内部プログラムのエラー)
|
||||
EXX01XX : トークンエラー(トークン認証関連)
|
||||
EXX02XX : DBエラー(DB関連)
|
||||
EXX03XX : ADB2Cエラー(DB関連)
|
||||
*/
|
||||
export const errorCodes = [
|
||||
"E009999", // 汎用エラー
|
||||
"E000101", // トークン形式不正エラー
|
||||
"E000102", // トークン有効期限切れエラー
|
||||
"E000103", // トークン非アクティブエラー
|
||||
"E000104", // トークン署名エラー
|
||||
"E000105", // トークン発行元エラー
|
||||
"E000106", // トークンアルゴリズムエラー
|
||||
"E000107", // トークン不足エラー
|
||||
"E000108", // トークン権限エラー
|
||||
"E000301", // ADB2Cへのリクエスト上限超過エラー
|
||||
"E010001", // パラメータ形式不正エラー
|
||||
"E010201", // 未認証ユーザエラー
|
||||
"E010202", // 認証済ユーザエラー
|
||||
"E010203", // 管理ユーザ権限エラー
|
||||
"E010204", // ユーザ不在エラー
|
||||
"E010205", // DBのRoleが想定外の値エラー
|
||||
"E010206", // DBのTierが想定外の値エラー
|
||||
"E010207", // ユーザーのRole変更不可エラー
|
||||
"E010208", // ユーザーの暗号化パスワード不足エラー
|
||||
"E010209", // ユーザーの同意済み利用規約バージョンが最新でないエラー
|
||||
"E010301", // メールアドレス登録済みエラー
|
||||
"E010302", // authorId重複エラー
|
||||
"E010401", // PONumber重複エラー
|
||||
"E010501", // アカウント不在エラー
|
||||
"E010502", // アカウント情報変更不可エラー
|
||||
"E010503", // 代行操作不許可エラー
|
||||
"E010601", // タスク変更不可エラー(タスクが変更できる状態でない、またはタスクが存在しない)
|
||||
"E010602", // タスク変更権限不足エラー
|
||||
"E010603", // タスク不在エラー
|
||||
"E010701", // Blobファイル不在エラー
|
||||
"E010801", // ライセンス不在エラー
|
||||
"E010802", // ライセンス取り込み済みエラー
|
||||
"E010803", // ライセンス発行済みエラー
|
||||
"E010804", // ライセンス数不足エラー
|
||||
"E010805", // ライセンス有効期限切れエラー
|
||||
"E010806", // ライセンス割り当て不可エラー
|
||||
"E010807", // ライセンス割り当て解除不可エラー
|
||||
"E010808", // ライセンス注文キャンセル不可エラー
|
||||
"E010809", // ライセンス発行キャンセル不可エラー(ステータスが変えられている場合)
|
||||
"E010810", // ライセンス発行キャンセル不可エラー(発行から一定期間経過した場合)
|
||||
"E010811", // ライセンス発行キャンセル不可エラー(発行したライセンスが割り当てされている場合)
|
||||
"E010908", // タイピストグループ不在エラー
|
||||
"E010909", // タイピストグループ名重複エラー
|
||||
"E011001", // ワークタイプ重複エラー
|
||||
"E011002", // ワークタイプ登録上限超過エラー
|
||||
"E011003", // ワークタイプ不在エラー
|
||||
"E011004", // ワークタイプ使用中エラー
|
||||
"E013001", // ワークフローのAuthorIDとWorktypeIDのペア重複エラー
|
||||
"E013002", // ワークフロー不在エラー
|
||||
"E014001", // ユーザー削除エラー(削除しようとしたユーザーがすでに削除済みだった)
|
||||
"E014002", // ユーザー削除エラー(削除しようとしたユーザーが管理者だった)
|
||||
"E014003", // ユーザー削除エラー(削除しようとしたAuthorのAuthorIDがWorkflowに指定されていた)
|
||||
"E014004", // ユーザー削除エラー(削除しようとしたTypistがWorkflowのTypist候補として指定されていた)
|
||||
"E014005", // ユーザー削除エラー(削除しようとしたTypistがUserGroupに所属していた)
|
||||
"E014006", // ユーザー削除エラー(削除しようとしたユーザが所有者の未完了のタスクが残っている)
|
||||
"E014007", // ユーザー削除エラー(削除しようとしたユーザーが有効なライセンスを持っていた)
|
||||
"E014009", // ユーザー削除エラー(削除しようとしたTypistが未完了のタスクのルーティングに設定されている)
|
||||
"E015001", // タイピストグループ削除済みエラー
|
||||
"E015002", // タイピストグループがワークフローに紐づいているエラー
|
||||
"E015003", // タイピストグループがルーティングされているエラー
|
||||
"E016001", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがすでに削除済みだった)
|
||||
"E016002", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルがWorkflowに指定されていた)
|
||||
"E016003", // テンプレートファイル削除エラー(削除しようとしたテンプレートファイルが未完了のタスクに紐づいていた)
|
||||
] as const;
|
||||
3
dictation_function/src/common/errors/index.ts
Normal file
3
dictation_function/src/common/errors/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./code";
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
9
dictation_function/src/common/errors/types.ts
Normal file
9
dictation_function/src/common/errors/types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { errorCodes } from "./code";
|
||||
|
||||
export type ErrorObject = {
|
||||
message: string;
|
||||
code: ErrorCodeType;
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
export type ErrorCodeType = typeof errorCodes[number];
|
||||
101
dictation_function/src/common/errors/utils.ts
Normal file
101
dictation_function/src/common/errors/utils.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { AxiosError } from "axios";
|
||||
import { isError } from "lodash";
|
||||
import { ErrorResponse } from "../../api";
|
||||
import { errorCodes } from "./code";
|
||||
import { ErrorCodeType, ErrorObject } from "./types";
|
||||
|
||||
export const createErrorObject = (error: unknown): ErrorObject => {
|
||||
// 最低限通常のエラーかを判定
|
||||
// Error以外のものがthrowされた場合
|
||||
// 基本的にないはずだがプログラム上あるので拾う
|
||||
if (!isError(error)) {
|
||||
return {
|
||||
message: "not error type.",
|
||||
code: "E009999",
|
||||
};
|
||||
}
|
||||
|
||||
// Axiosエラー 通信してのエラーであるかを判定
|
||||
if (!isAxiosError(error)) {
|
||||
return {
|
||||
message: "not axios error.",
|
||||
code: "E009999",
|
||||
};
|
||||
}
|
||||
|
||||
const errorResponse = error.response;
|
||||
if (!errorResponse) {
|
||||
return {
|
||||
message: error.message,
|
||||
code: "E009999",
|
||||
statusCode: errorResponse,
|
||||
};
|
||||
}
|
||||
|
||||
const { data } = errorResponse;
|
||||
|
||||
// 想定しているエラーレスポンスの型か判定
|
||||
if (!isErrorResponse(data)) {
|
||||
return {
|
||||
message: error.message,
|
||||
code: "E009999",
|
||||
statusCode: errorResponse.status,
|
||||
};
|
||||
}
|
||||
|
||||
const { message, code } = data;
|
||||
|
||||
// 想定しているエラーコードかを判定
|
||||
if (!isErrorCode(code)) {
|
||||
return {
|
||||
message,
|
||||
code: "E009999",
|
||||
statusCode: errorResponse.status,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message,
|
||||
code,
|
||||
statusCode: errorResponse.status,
|
||||
};
|
||||
};
|
||||
|
||||
const isAxiosError = (e: unknown): e is AxiosError => {
|
||||
const error = e as AxiosError;
|
||||
return error?.isAxiosError ?? false;
|
||||
};
|
||||
|
||||
const isErrorResponse = (error: unknown): error is ErrorResponse => {
|
||||
const errorResponse = error as ErrorResponse;
|
||||
if (
|
||||
errorResponse === undefined ||
|
||||
errorResponse.message === undefined ||
|
||||
errorResponse.code === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isErrorCode = (errorCode: string): errorCode is ErrorCodeType =>
|
||||
errorCodes.includes(errorCode as ErrorCodeType);
|
||||
|
||||
export const isErrorObject = (
|
||||
data: unknown
|
||||
): data is { error: ErrorObject } => {
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"error" in data &&
|
||||
typeof (data as { error: ErrorObject }).error === "object" &&
|
||||
typeof (data as { error: ErrorObject }).error.message === "string" &&
|
||||
typeof (data as { error: ErrorObject }).error.code === "string" &&
|
||||
(typeof (data as { error: ErrorObject }).error.statusCode === "number" ||
|
||||
(data as { error: ErrorObject }).error.statusCode === undefined)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
3
dictation_function/src/common/jwt/index.ts
Normal file
3
dictation_function/src/common/jwt/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { isVerifyError, sign, verify, decode, getJwtKey } from "./jwt";
|
||||
|
||||
export { isVerifyError, sign, verify, decode, getJwtKey };
|
||||
250
dictation_function/src/common/jwt/jwt.spec.ts
Normal file
250
dictation_function/src/common/jwt/jwt.spec.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import { sign, verify, isVerifyError } from "./jwt";
|
||||
import base64url from "base64url";
|
||||
|
||||
test("success sign and verify", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const payload = verify<{ value: "testvalue" }>(token, publicKey);
|
||||
if (isVerifyError(payload)) {
|
||||
throw new Error(`${payload.reason} | ${payload.message}`);
|
||||
}
|
||||
|
||||
expect(payload.value).toBe("testvalue");
|
||||
});
|
||||
|
||||
test("failed sign and verify (jwt expired)", () => {
|
||||
// 有効期限を0秒にすることで、検証を行った時点で有効期限切れにする
|
||||
const token = sign({ value: "testvalue" }, 0, privateKey);
|
||||
const payload = verify<{ value: "testvalue" }>(token, publicKey);
|
||||
if (!isVerifyError(payload)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(payload.reason).toBe("ExpiredError");
|
||||
});
|
||||
|
||||
test("failed sign and verify (invalid key pair)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
// 秘密鍵と対ではない公開鍵を使用して検証する
|
||||
const payload = verify<{ value: "testvalue" }>(token, anotherPublicKey);
|
||||
if (!isVerifyError(payload)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(payload.reason).toBe("InvalidToken");
|
||||
expect(payload.message).toBe("invalid signature");
|
||||
});
|
||||
|
||||
test("failed sign and verify (invalid public key)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
// 公開鍵の形式になっていない文字列を使用して検証する
|
||||
const payload = verify<{ value: "testvalue" }>(token, fakePublicKey);
|
||||
if (!isVerifyError(payload)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(payload.reason).toBe("InvalidToken");
|
||||
expect(payload.message).toBe(
|
||||
"secretOrPublicKey must be an asymmetric key when using RS256"
|
||||
);
|
||||
});
|
||||
|
||||
test("failed sign (invalid private key)", () => {
|
||||
expect(() => {
|
||||
// 不正な秘密鍵で署名しようとする場合はエラーがthrowされる
|
||||
sign({ value: "testvalue" }, 5 * 60, fakePrivateKey);
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test("success rewrite-token verify (as is)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const { header, payload, verifySignature } = splitToken(token);
|
||||
|
||||
{
|
||||
// 何も操作せずに構築しなおした場合、成功する
|
||||
const validToken = rebuildToken(header, payload, verifySignature);
|
||||
|
||||
const value = verify<{ value: string }>(validToken, publicKey);
|
||||
if (isVerifyError(value)) {
|
||||
throw new Error(`${value.reason} | ${value.message}`);
|
||||
}
|
||||
|
||||
expect(value.value).toBe("testvalue");
|
||||
}
|
||||
});
|
||||
|
||||
test("failed rewrite-token verify (override algorithm)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const { payload, verifySignature } = splitToken(token);
|
||||
|
||||
{
|
||||
// 検証アルゴリズムを「検証なし」に書き換える
|
||||
const headerObject = { alg: "none" };
|
||||
const payloadObject = JSON.parse(payload) as {
|
||||
value: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
// 内容を操作して構築しなおした場合、失敗する
|
||||
const customToken = rebuildToken(
|
||||
JSON.stringify(headerObject),
|
||||
JSON.stringify(payloadObject),
|
||||
verifySignature
|
||||
);
|
||||
|
||||
const value = verify<{ value: string }>(customToken, publicKey);
|
||||
if (!isVerifyError(value)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(value.reason).toBe("InvalidToken");
|
||||
expect(value.message).toBe("invalid algorithm");
|
||||
}
|
||||
});
|
||||
|
||||
test("failed rewrite-token verify (override expire)", () => {
|
||||
const token = sign({ value: "testvalue" }, 5 * 60, privateKey);
|
||||
const { header, payload, verifySignature } = splitToken(token);
|
||||
|
||||
{
|
||||
// expの値を操作する
|
||||
const payloadObject = JSON.parse(payload) as {
|
||||
value: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
payloadObject.exp = payloadObject.exp + 100000;
|
||||
|
||||
// 内容を操作して構築しなおした場合、失敗する
|
||||
const customToken = rebuildToken(
|
||||
header,
|
||||
JSON.stringify(payloadObject),
|
||||
verifySignature
|
||||
);
|
||||
|
||||
const value = verify<{ value: string }>(customToken, publicKey);
|
||||
if (!isVerifyError(value)) {
|
||||
throw new Error(JSON.stringify(payload));
|
||||
}
|
||||
expect(value.reason).toBe("InvalidToken");
|
||||
expect(value.message).toBe("invalid signature");
|
||||
}
|
||||
});
|
||||
|
||||
// JWT改竄テスト用ユーティリティ
|
||||
const splitToken = (
|
||||
token: string
|
||||
): { header: string; payload: string; verifySignature: string } => {
|
||||
const splited = token.split(".");
|
||||
|
||||
const header = base64url.decode(splited[0]);
|
||||
const payload = base64url.decode(splited[1]);
|
||||
const verifySignature = splited[2];
|
||||
return { header, payload, verifySignature };
|
||||
};
|
||||
|
||||
// JWT改竄テスト用ユーティリティ
|
||||
const rebuildToken = (
|
||||
header: string,
|
||||
payload: string,
|
||||
verifySignature: string
|
||||
): string => {
|
||||
const rebuild_header = base64url.encode(header);
|
||||
const rebuild_payload = base64url.encode(payload);
|
||||
return `${rebuild_header}.${rebuild_payload}.${verifySignature}`;
|
||||
};
|
||||
|
||||
// テスト用に生成した秘密鍵
|
||||
const privateKey = [
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
"MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F",
|
||||
"GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a",
|
||||
"fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa",
|
||||
"FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq",
|
||||
"zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF",
|
||||
"+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN",
|
||||
"LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq",
|
||||
"L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0",
|
||||
"WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE",
|
||||
"x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm",
|
||||
"+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX",
|
||||
"cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc",
|
||||
"BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H",
|
||||
"IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+",
|
||||
"y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb",
|
||||
"iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9",
|
||||
"BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95",
|
||||
"mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/",
|
||||
"O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s",
|
||||
"xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust",
|
||||
"nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT",
|
||||
"tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK",
|
||||
"YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM",
|
||||
"iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk",
|
||||
"JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==",
|
||||
"-----END RSA PRIVATE KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// テスト用に生成した公開鍵
|
||||
const publicKey = [
|
||||
"-----BEGIN PUBLIC KEY-----",
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsTVLNpW0/FzVCU7qo1DD",
|
||||
"jOkYWx6s/jE56YOOc3UzaaG/zb1FGyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23",
|
||||
"tL7p3PS0ZCsLeggcyLctbJAzLy/afF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc5",
|
||||
"7Tli2x8NiOZr5dafs3vMuIIKNsBaFAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxD",
|
||||
"alw5DCd0mmdY0vrbRsgkbej0ZzzqzukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn2",
|
||||
"1AIcn8P3rKZmDyPH+9KNfWC8+ubF+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3a",
|
||||
"IQIDAQAB",
|
||||
"-----END PUBLIC KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// テスト用に作成した、違う秘密鍵から生成した公開鍵
|
||||
const anotherPublicKey = [
|
||||
"-----BEGIN PUBLIC KEY-----",
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3",
|
||||
"2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig",
|
||||
"n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9",
|
||||
"RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk",
|
||||
"EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x",
|
||||
"2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81",
|
||||
"KQIDAQAB",
|
||||
"-----END PUBLIC KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// 秘密鍵のように見えるが想定する形式と違う
|
||||
const fakePrivateKey = [
|
||||
"-----BAGIN RSA PRIVATE KEY-----",
|
||||
"MIIEpAIBAAKCAQEAsTVLNpW0/FzVCU7qo1DDjOkYWx6s/jE56YOOc3UzaaG/zb1F",
|
||||
"GyfRoUUgS4DnQxPNz9oM63RpQlhvG6UCwx23tL7p3PS0ZCsLeggcyLctbJAzLy/a",
|
||||
"fF9ABoreorqp/AaEs+Vdwbykb+M+nB2Sxsc57Tli2x8NiOZr5dafs3vMuIIKNsBa",
|
||||
"FAugFrd2ApxXR04jBRAorZRRFPtECE7D+hxDalw5DCd0mmdY0vrbRsgkbej0Zzzq",
|
||||
"zukJVXTMjy1YScqi3I9gLx2hLVmpK76Gtxn21AIcn8P3rKZmDyPH+9KNfWC8+ubF",
|
||||
"+VuY6nItlCgiSyTKErAp6M9pyRHKbPpdUM3aIQIDAQABAoIBAQCk7fkmwIdGKhCN",
|
||||
"LUns3opiZ8AnbpGLs702vR6kDvze35BoqDPdZl4RPwkjvMGBCLmRLly/+ATPnwcq",
|
||||
"L5Y2iz4jl1yKLaaHZBi2Zz6DARnh5QP+cwdiojQw4qb7xcfXrSltVZjBbBWPnWz0",
|
||||
"WAH3yAz94V9Emc47EFpz/CF/J0YOokxY8GlR4cwfK6NE0goAjzmatwV3IVFeR/eE",
|
||||
"x6JZAmd/0HMfOn3k/NumAMCJXKnZMQBAMQ3AduTO2lbZm+29yBqymtzTGFjrj0gm",
|
||||
"+E/ibD8vVzh0toPvUfPIqetdRT8vkUJ5UHhAkz9Vzvqhr6BhYhc2ft0x/z7HpaiX",
|
||||
"cDqnaRLBAoGBAODdPEktK1VOVXhOuikZBUHXU25iQdQRbM4kCtWiE8lBZ/f+6OPc",
|
||||
"BN+OedYMDhpFe/oFqGU4t610SPO1CdVRPnWHhMSabjh9G3gqOZjSW5tEAgT2wi+H",
|
||||
"IOVXnsos1qCMFdXWgVZw6F8wNcui9VabGic/EOqMRihEeSOjcradTSQFAoGBAMm+",
|
||||
"y2wZ8usanIDzADgTJnA4kBZzhIxK6qcPf3tPVXKuFUOFWwzGiDXeXTwM0sWN7kGb",
|
||||
"iymqhTWlYETQ3C6jPXTJiyOSco1rw45wO+xSHeQvUzXpk+9whbVAlhTcoVGiKz+9",
|
||||
"BS7+3+lKtBzXDNADxQfSGjiGb+ceilBGLV+WurRtAoGAPxn2a/aP/X1hAMTe+t95",
|
||||
"mTNqx0Qtguxs4yA8Jh04fjarjW1sP10jxPR/fjCd2IN9OflSey1CZhuGyVUZcFI/",
|
||||
"O84O1PkdSx7YkY0P4rHNYTHhezEf5yR9d75x4fxZMm59RifO3coLe4LU5dNSE76s",
|
||||
"xSyue5NnsK8ea4DXlSVpW10CgYAfHz3GWWJt/lbyVYpNHDcrzK39qKhj9BKq3ust",
|
||||
"nJlz7YL+PY5ENERC+yCq6NeC/lgo6tPXA6U1F2P4ebfdwfTzFTxPqoHdayhpysqT",
|
||||
"tD9EOkC96mCV6WfXBDWi1j5Ul43QcVphW5QzKwEKCerCFDLK+BBvc93Da6SuqYTK",
|
||||
"YDhBKQKBgQDKtNe8CjHRvkWoyKErMMpv5D0ce/yWq+oAaoqW1QKwngPyaiDeDwqM",
|
||||
"iOJzQxtvK4YqMYQdkgj5VLfWzeazd28RLODZua6phe776zuUv93LHTvYq/8RZfhk",
|
||||
"JIQJ7GETBnHmoTemwmJiSdVDsjJdtsyR4XRjIDNR5bGe7NNbZJpCUw==",
|
||||
"-----END RSA PRIVATE KEY-----",
|
||||
].join("\n");
|
||||
|
||||
// 公開鍵のように見えるが想定する形式と違う
|
||||
const fakePublicKey = [
|
||||
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt1WsgrjpjsEfRa7vqlR3",
|
||||
"2mGxErXpvC+uRQnFtSXdP4tEYicPb1cNFUcu5xW6attTyzKHKMzwJrvmKEKVYGig",
|
||||
"n43rM+UyW79DNOQWQQblCHAc3hMolLWC+Tkw7xL4JhzZLH0rm5DF52YNYSicV1S9",
|
||||
"RpxYEeyHUa+ExV82lT47ySWAwg+yPwtDeDPMbOxHXqyw1wdqR2WVuxsQBaIRQgMk",
|
||||
"EL/qObQjA4e5jOOwERRvVLxzjhnldUZcG0cYGDfjPTewRYfCeXzMx2YM4Uo0vx0x",
|
||||
"2ZIY+im061GvfugX4/31xB5YEi+62qIwuSL5UpKjMv5yx1cvIqO76Ro3XNwsR+81",
|
||||
"KQIDAQAB",
|
||||
].join("\n");
|
||||
130
dictation_function/src/common/jwt/jwt.ts
Normal file
130
dictation_function/src/common/jwt/jwt.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import * as jwt from "jsonwebtoken";
|
||||
// XXX: decodeがうまく使えないことがあるので応急対応 バージョン9以降だとなる?
|
||||
import { decode as jwtDecode } from "jsonwebtoken";
|
||||
|
||||
export type VerifyError = {
|
||||
reason: "ExpiredError" | "InvalidToken" | "InvalidTimeStamp" | "Unknown";
|
||||
message: string;
|
||||
};
|
||||
|
||||
export const isVerifyError = (arg: unknown): arg is VerifyError => {
|
||||
const value = arg as VerifyError;
|
||||
if (value.message === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.reason === undefined) {
|
||||
return false;
|
||||
}
|
||||
switch (value.reason) {
|
||||
case "ExpiredError":
|
||||
case "InvalidTimeStamp":
|
||||
case "InvalidToken":
|
||||
case "Unknown":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Payloadと秘密鍵を使用して署名されたJWTを生成します
|
||||
* @param {T} payload payloadの型
|
||||
* @param {number} expirationSeconds トークンの有効期限(秒)
|
||||
* @param {string} privateKey 署名に使用する秘密鍵
|
||||
* @return {string} 署名済みトークン
|
||||
* @throws {Error} 秘密鍵の形式が間違っている等の理由が格納されたErrorオブジェクト
|
||||
*/
|
||||
export const sign = <T extends object>(
|
||||
payload: T,
|
||||
expirationSeconds: number,
|
||||
privateKey: string
|
||||
): string => {
|
||||
try {
|
||||
const token = jwt.sign(payload, privateKey, {
|
||||
expiresIn: expirationSeconds,
|
||||
algorithm: "RS256",
|
||||
});
|
||||
return token;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* tokenと公開鍵を使用して検証済みJWTのpayloadを取得します
|
||||
* @param {string} token JWT
|
||||
* @param {string} publicKey 検証に使用する公開鍵
|
||||
* @return {T | VerifyError} Payload または 検証エラーの内容を表すオブジェクト
|
||||
*/
|
||||
export const verify = <T extends object>(
|
||||
token: string,
|
||||
publicKey: string
|
||||
): T | VerifyError => {
|
||||
try {
|
||||
const payload = jwt.verify(token, publicKey, {
|
||||
algorithms: ["RS256"],
|
||||
}) as T;
|
||||
return payload;
|
||||
} catch (e) {
|
||||
if (e instanceof jwt.TokenExpiredError) {
|
||||
return {
|
||||
reason: "ExpiredError",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.NotBeforeError) {
|
||||
return {
|
||||
reason: "InvalidTimeStamp",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.JsonWebTokenError) {
|
||||
return {
|
||||
reason: "InvalidToken",
|
||||
message: e.message,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
reason: "Unknown",
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* tokenから未検証のJWTのpayloadを取得します
|
||||
* @param {string} token JWT
|
||||
* @return {T | VerifyError} Payload または デコードエラーの内容を表すオブジェクト
|
||||
*/
|
||||
export const decode = <T extends object>(token: string): T | VerifyError => {
|
||||
try {
|
||||
const payload = jwtDecode(token, {
|
||||
json: true,
|
||||
}) as T;
|
||||
return payload;
|
||||
} catch (e) {
|
||||
if (e instanceof jwt.TokenExpiredError) {
|
||||
return {
|
||||
reason: "ExpiredError",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.NotBeforeError) {
|
||||
return {
|
||||
reason: "InvalidTimeStamp",
|
||||
message: e.message,
|
||||
};
|
||||
} else if (e instanceof jwt.JsonWebTokenError) {
|
||||
return {
|
||||
reason: "InvalidToken",
|
||||
message: e.message,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
reason: "Unknown",
|
||||
message: e.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getJwtKey = (key: string): string => key.replace(/\\n/g, "\n");
|
||||
32
dictation_function/src/common/jwt/types.ts
Normal file
32
dictation_function/src/common/jwt/types.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export type AccessToken = {
|
||||
/**
|
||||
* 外部認証サービスの識別子(代行者)
|
||||
*/
|
||||
delegateUserId?: string | undefined;
|
||||
/**
|
||||
* 外部認証サービスの識別子
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* 半角スペース区切りのRoleを表現する文字列(ex. "author admin")
|
||||
*/
|
||||
role: string;
|
||||
/**
|
||||
* アカウントの階層情報(1~5までの半角数字)
|
||||
*/
|
||||
tier: number;
|
||||
};
|
||||
|
||||
// システムの内部で発行し、外部に公開しないトークン
|
||||
// システム間通信用(例: Azure Functions→AppService)に使用する
|
||||
export type SystemAccessToken = {
|
||||
/**
|
||||
* トークンの発行者名(ログ記録用)
|
||||
*/
|
||||
systemName: string;
|
||||
|
||||
/**
|
||||
* 付加情報を 文字情報として格納できる
|
||||
*/
|
||||
context?: string;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user